mirror of
https://github.com/SqrtMinusOne/biome.git
synced 2025-12-10 14:35:13 +03:00
commit
a244702c55
6 changed files with 384 additions and 5 deletions
31
README.org
31
README.org
|
|
@ -72,6 +72,37 @@ Table formatting can be configured with =biome-grid-format=; check the docstring
|
|||
biome-grid-format))
|
||||
#+end_src
|
||||
|
||||
* Composite queries
|
||||
The package also allows executing multiple queries at once to join their results. This can be useful for comparing weather in different locations or for viewing different reports about the same location.
|
||||
|
||||
Run =M-x biome-multi= to invoke the-multi query dialog.
|
||||
|
||||
[[./img/multi.png]]
|
||||
|
||||
(/yes, I've switched to a light theme since the time of the previous screenshot/)
|
||||
|
||||
Pressing =a= invokes the standard query dialog, where pressing =RET= returns to the root dialog, adding the query to the list. Pressing =RET= in the root dialog executes the queries in the list.
|
||||
|
||||
Queries are executed concurrently. The results are shown if all queries have been successfully completed.
|
||||
|
||||
=P= generates a preset defintion for the current query:
|
||||
#+begin_src emacs-lisp
|
||||
(biome-def-multi-preset biome-query-preset-601
|
||||
(((:name . "Air Quality")
|
||||
(:group . "hourly")
|
||||
(:params
|
||||
("hourly" "uv_index" "european_aqi")
|
||||
("longitude" . 24.93545)
|
||||
("latitude" . 60.16952)))
|
||||
((:name . "Weather Forecast")
|
||||
(:group . "hourly")
|
||||
(:params
|
||||
("hourly" "weathercode" "snowfall" "showers" "rain" "temperature_2m")
|
||||
("longitude" . 24.93545)
|
||||
("latitude" . 60.16952)))))
|
||||
#+end_src
|
||||
Just note that the macro is called =biome-def-multi-preset=.
|
||||
|
||||
* Implementation notes
|
||||
This isn't the most complicated thing I've done, but it's probably the most over-engineered one.
|
||||
|
||||
|
|
|
|||
37
biome-api.el
37
biome-api.el
|
|
@ -128,5 +128,42 @@ called with QUERY and the data returned by the API as arguments."
|
|||
(cl-function (lambda (&key error-thrown response &allow-other-keys)
|
||||
(biome-api--show-error error-thrown response))))))
|
||||
|
||||
(defun biome-api-get-multiple (queries callback)
|
||||
"Get data from Open Meteo API.
|
||||
|
||||
QUERIES is a list of forms as defined by `biome-query-current'. CALLBACK is
|
||||
called with QUERIES and the data returned by the API as arguments."
|
||||
(let (requests)
|
||||
(seq-map-indexed
|
||||
(lambda (query idx)
|
||||
(push
|
||||
(request (alist-get (alist-get :name query)
|
||||
biome-api-urls nil nil #'equal)
|
||||
:type "GET"
|
||||
:params (biome-api--get-params query)
|
||||
:parser #'json-read
|
||||
:success (cl-function
|
||||
(lambda (&allow-other-keys)
|
||||
;; I'm not sure why, but `request-response-done-p' for
|
||||
;; the current request returns nil. I don't
|
||||
;; know how stable this is, so...
|
||||
(let ((completed-count
|
||||
(cl-loop for i from 0
|
||||
for request in requests
|
||||
if (or (request-response-done-p request)
|
||||
(= i (- (length requests) 1 idx)))
|
||||
sum 1)))
|
||||
(message "Completed %d/%d requests"
|
||||
completed-count (length queries))
|
||||
(when (eq (length queries) completed-count)
|
||||
(funcall callback (copy-tree queries)
|
||||
(mapcar #'request-response-data
|
||||
(reverse requests)))))))
|
||||
:error
|
||||
(cl-function (lambda (&key error-thrown response &allow-other-keys)
|
||||
(biome-api--show-error error-thrown response))))
|
||||
requests))
|
||||
queries)))
|
||||
|
||||
(provide 'biome-api)
|
||||
;;; biome-api.el ends here
|
||||
|
|
|
|||
278
biome-multi.el
Normal file
278
biome-multi.el
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
;;; biome-multi.el --- Do multiple queries to Open Meteo -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2024 Korytov Pavel
|
||||
|
||||
;; Author: Korytov Pavel <thexcloud@gmail.com>
|
||||
;; Maintainer: Korytov Pavel <thexcloud@gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; This program is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Tools for doing multiple queries to Open Meteo.
|
||||
|
||||
;;; Code:
|
||||
(require 'biome-query)
|
||||
(require 'font-lock)
|
||||
(require 'transient)
|
||||
|
||||
(defvar biome-multi-query-current nil
|
||||
"Current query.
|
||||
|
||||
This is a list of forms as defined by `biome-query-current'.")
|
||||
|
||||
(defvar biome-multi--callback nil
|
||||
"Call this with the selected query.")
|
||||
|
||||
(defclass biome-multi--transient-report (transient-suffix)
|
||||
((transient :initform t))
|
||||
"A class to display the current report for `biome-multi'.")
|
||||
|
||||
(cl-defmethod transient-init-value ((_ biome-multi--transient-report))
|
||||
"A dummy method for `biome-multi--transient-report'."
|
||||
nil)
|
||||
|
||||
(cl-defmethod transient-format ((_ biome-multi--transient-report))
|
||||
"Format the current report for `biome-multi'."
|
||||
(if (seq-empty-p biome-multi-query-current)
|
||||
(propertize "Add at least one query" 'face 'error)
|
||||
(cl-loop for i from 0
|
||||
for query in biome-multi-query-current
|
||||
concat (propertize (format "Query #%d: %s\n" i
|
||||
(alist-get :name query))
|
||||
'face 'font-lock-keyword-face)
|
||||
concat (biome-query--format query)
|
||||
if (not (eq i (1- (length biome-multi-query-current))))
|
||||
concat "\n")))
|
||||
|
||||
(transient-define-infix biome-multi--transient-report-infix ()
|
||||
:class 'biome-multi--transient-report
|
||||
:key "~~1")
|
||||
|
||||
(defun biome-multi-add-query ()
|
||||
"Add new query to `biome-multi'."
|
||||
(interactive)
|
||||
(funcall-interactively
|
||||
#'biome-query
|
||||
(lambda (query)
|
||||
(if (seq-empty-p biome-multi-query-current)
|
||||
(setq biome-multi-query-current (list (copy-tree query)))
|
||||
(nconc biome-multi-query-current (list (copy-tree query))))
|
||||
(biome-multi-query biome-multi--callback))))
|
||||
|
||||
(defun biome-multi-reset ()
|
||||
"Reset `biome-multi'."
|
||||
(interactive)
|
||||
(setq biome-multi-query-current nil))
|
||||
|
||||
(defun biome-multi-edit (idx)
|
||||
"Edit query number IDX in `biome-multi'."
|
||||
(interactive "nQuery number: ")
|
||||
(when (or (< idx 0)
|
||||
(>= idx (length biome-multi-query-current)))
|
||||
(user-error "Invalid query number"))
|
||||
(setq biome-query-current (nth idx biome-multi-query-current))
|
||||
(setq biome-query--callback
|
||||
(lambda (query)
|
||||
(setf (nth idx biome-multi-query-current) query)
|
||||
(biome-multi-query biome-multi--callback)))
|
||||
(biome-query--section-open (alist-get :name biome-query-current)))
|
||||
|
||||
(defun biome-multi-remove (idx)
|
||||
"Remove query number IDX from `biome-multi'."
|
||||
(interactive "nQuery number: ")
|
||||
(when (or (< idx 0)
|
||||
(>= idx (length biome-multi-query-current)))
|
||||
(user-error "Invalid query number"))
|
||||
(setq biome-multi-query-current
|
||||
(cl-loop for query in biome-multi-query-current
|
||||
for i from 0
|
||||
unless (eq i idx)
|
||||
collect query)))
|
||||
|
||||
(defun biome-multi-exec ()
|
||||
"Process the query made by `biome-multi-query'."
|
||||
(interactive)
|
||||
(when (seq-empty-p biome-multi-query-current)
|
||||
(user-error "No queries to execute"))
|
||||
(funcall biome-multi--callback biome-multi-query-current))
|
||||
|
||||
(defun biome-multi--generate-preset ()
|
||||
"Generate a preset for the current multi-query."
|
||||
(interactive)
|
||||
(let ((buf (generate-new-buffer "*biome-preset*")))
|
||||
(with-current-buffer buf
|
||||
(emacs-lisp-mode)
|
||||
(insert ";; Add this to your config\n")
|
||||
(insert (pp-to-string `(biome-def-multi-preset ,(gensym "biome-query-preset-")
|
||||
,biome-multi-query-current))))
|
||||
(switch-to-buffer buf)))
|
||||
|
||||
(transient-define-prefix biome-multi-query (callback)
|
||||
["Open Meteo Multi Query"
|
||||
(biome-multi--transient-report-infix)]
|
||||
["Queries"
|
||||
:class transient-row
|
||||
("a" "Add query" biome-multi-add-query :transient transient--do-replace)
|
||||
("e" "Edit query" biome-multi-edit :transient t)
|
||||
("d" "Delete query" biome-multi-remove :transient t)]
|
||||
["Actions"
|
||||
:class transient-row
|
||||
("RET" "Run" biome-multi-exec)
|
||||
("P" "Generate preset definition" biome-multi--generate-preset)
|
||||
("R" "Reset" biome-multi-reset :transient t)
|
||||
("q" "Quit" transient-quit-one)]
|
||||
(interactive (list nil))
|
||||
(unless callback
|
||||
(error "Callback is not set. Run M-x `biome-multi' instead"))
|
||||
(setq biome-multi--callback callback)
|
||||
(transient-setup 'biome-multi-query))
|
||||
|
||||
(defun biome-multi--unique-names-grouped (names-by-group group-names)
|
||||
"Make names unique in accordance with GROUP-NAMES.
|
||||
|
||||
NAMES-BY-GROUP is a list of lists of names. GROUP-NAMES is a list
|
||||
of group names. The function returns a hash table mapping
|
||||
original names to unique names."
|
||||
(let ((name-occurences (make-hash-table :test #'equal))
|
||||
(names-mapping (make-hash-table :test #'equal)))
|
||||
(cl-loop for names in names-by-group
|
||||
do (cl-loop for name in names
|
||||
do (puthash name
|
||||
(1+ (gethash name name-occurences 0))
|
||||
name-occurences)))
|
||||
(cl-loop for names in names-by-group
|
||||
for group-name in group-names
|
||||
do (cl-loop
|
||||
for name in names
|
||||
for occurences = (gethash name name-occurences)
|
||||
do (puthash (format "%s--%s" group-name name)
|
||||
(if (= occurences 1)
|
||||
name
|
||||
(format "%s_%s" name
|
||||
(replace-regexp-in-string
|
||||
(rx space) "_" (downcase group-name))))
|
||||
names-mapping)))
|
||||
names-mapping))
|
||||
|
||||
(defun biome-multi--unique-names (names)
|
||||
"Make NAMES unique.
|
||||
|
||||
NAMES is a list of strings. The return value is a list of
|
||||
strings as well."
|
||||
(let ((name-occurences (make-hash-table :test #'equal))
|
||||
(added-occurences (make-hash-table :test #'equal)))
|
||||
(cl-loop for name in names
|
||||
do (puthash name
|
||||
(1+ (gethash name name-occurences 0))
|
||||
name-occurences))
|
||||
(cl-loop for name in names
|
||||
for occurences = (gethash name name-occurences)
|
||||
for added = (gethash name added-occurences)
|
||||
collect (if (= occurences 1)
|
||||
name
|
||||
(format "%s_%d" name
|
||||
(puthash
|
||||
name
|
||||
(1+ (or added 0))
|
||||
added-occurences))))))
|
||||
|
||||
(defun biome-multi--join-results (queries query-names vars-mapping results)
|
||||
"Join RESULTS of QUERIES by time.
|
||||
|
||||
Time has be in a string format, comparable by `string-lessp'.
|
||||
|
||||
QUERIES is a list of forms as defined by `biome-query-current'.
|
||||
QUERY-NAMES is a list of query names, made unique. VARS-MAPPING is
|
||||
the result of `biome-multi--unique-names-grouped' on the list of
|
||||
variables. RESULTS is a list of responses from Open Meteo.
|
||||
|
||||
This function returns the results field mimicking the one returned
|
||||
by Open Meteo."
|
||||
(let ((times (make-hash-table :test #'equal))
|
||||
(var-values-per-time (make-hash-table :test #'equal)))
|
||||
(cl-loop for result in results
|
||||
for query in queries
|
||||
for query-name in query-names
|
||||
for group-name = (alist-get :group query)
|
||||
for vars-field = (intern group-name)
|
||||
for times-vector = (thread-last
|
||||
result (alist-get vars-field) (alist-get 'time))
|
||||
do (cl-loop for time across times-vector
|
||||
do (puthash time t times))
|
||||
do (cl-loop for (var-name . values) in (seq-filter
|
||||
(lambda (v) (not (eq 'time (car v))))
|
||||
(alist-get vars-field result))
|
||||
for mapped-var-name =
|
||||
(gethash (format "%s--%s" query-name var-name) vars-mapping)
|
||||
for var-values = (make-hash-table :test #'equal)
|
||||
do (cl-loop for time across times-vector
|
||||
for value across values
|
||||
do (puthash time value var-values))
|
||||
do (puthash mapped-var-name var-values var-values-per-time)))
|
||||
(let ((times-sorted (seq-sort #'string-lessp (hash-table-keys times))))
|
||||
`((time . ,(vconcat times-sorted))
|
||||
,@(cl-loop for var-name being the hash-keys of var-values-per-time
|
||||
using (hash-values var-values)
|
||||
collect
|
||||
(cons (intern var-name)
|
||||
(vconcat
|
||||
(cl-loop for time in times-sorted
|
||||
collect (gethash time var-values)))))))))
|
||||
|
||||
(defun biome-multi--merge (queries results)
|
||||
"Merge QUERIES into one query.
|
||||
|
||||
QUERIES is a list of forms as defined by `biome-query-current'. RESULTS
|
||||
is a list of responses from Open Meteo.
|
||||
|
||||
The function mimicks the response of Open Meteo, but only insofar
|
||||
as it is necessary for `biome-grid'."
|
||||
(let* ((vars-by-group
|
||||
(cl-loop for query in queries
|
||||
for group = (alist-get :group query)
|
||||
collect (alist-get group (alist-get :params query)
|
||||
nil nil #'string-equal)))
|
||||
(query-names
|
||||
(biome-multi--unique-names
|
||||
(cl-loop for query in queries
|
||||
collect (alist-get :name query))))
|
||||
(vars-mapping (biome-multi--unique-names-grouped vars-by-group query-names)))
|
||||
`(((:name . "Multi Query")
|
||||
(:group . "multi")
|
||||
(:params . (("multi" .
|
||||
,(cl-loop for var-name being the hash-values of vars-mapping
|
||||
collect var-name)))))
|
||||
((multi_units
|
||||
. ,(cons
|
||||
(cons 'time "iso8601")
|
||||
(cl-loop
|
||||
for result in results
|
||||
for query in queries
|
||||
for query-name in query-names
|
||||
for group-name = (alist-get :group query)
|
||||
for units-field = (intern (format "%s_units" group-name))
|
||||
append (cl-loop
|
||||
for (var-name . unit) in (alist-get units-field result)
|
||||
unless (equal var-name 'time)
|
||||
collect (cons (intern
|
||||
(gethash (format "%s--%s" query-name var-name)
|
||||
vars-mapping))
|
||||
unit)))))
|
||||
(multi . ,(biome-multi--join-results queries query-names vars-mapping results))))))
|
||||
|
||||
(provide 'biome-multi)
|
||||
;;; biome-multi.el ends here
|
||||
|
|
@ -175,12 +175,14 @@ KEY is the api key of the variable. VAR-NAMES is the output of
|
|||
(capitalize (replace-regexp-in-string
|
||||
(regexp-quote "_") " " key))))
|
||||
|
||||
(cl-defmethod transient-format ((_obj biome-query--transient-report))
|
||||
"Format the `biome-query-current'."
|
||||
(let ((group (alist-get :group biome-query-current))
|
||||
(defun biome-query--format (query)
|
||||
"Format QUERY for display.
|
||||
|
||||
QUERY is a form as defined by `transient-define-prefix'."
|
||||
(let ((group (alist-get :group query))
|
||||
(var-names (biome-query--get-var-names-cache))
|
||||
lat lon group-vars line-vars vars)
|
||||
(dolist (item (alist-get :params biome-query-current))
|
||||
(dolist (item (alist-get :params query))
|
||||
(cond
|
||||
((stringp item)
|
||||
(push (biome-query--get-header item var-names) vars))
|
||||
|
|
@ -251,6 +253,10 @@ KEY is the api key of the variable. VAR-NAMES is the output of
|
|||
(when line-vars
|
||||
(concat (mapconcat #'identity line-vars "\n") "\n")))))
|
||||
|
||||
(cl-defmethod transient-format ((_obj biome-query--transient-report))
|
||||
"Format the `biome-query-current'."
|
||||
(biome-query--format biome-query-current))
|
||||
|
||||
(transient-define-infix biome-query--transient-report-infix ()
|
||||
:class 'biome-query--transient-report
|
||||
:key "~~1")
|
||||
|
|
@ -953,7 +959,7 @@ SUFFIXES is a list of suffix definitions."
|
|||
"Process the query made by `biome-query'."
|
||||
(interactive)
|
||||
(unless biome-query--callback
|
||||
(user-error "Biome-query--callback is not set"))
|
||||
(error "Biome-query--callback is not set"))
|
||||
(funcall biome-query--callback biome-query-current))
|
||||
|
||||
(defun biome-query--generate-preset ()
|
||||
|
|
|
|||
27
biome.el
27
biome.el
|
|
@ -40,6 +40,7 @@
|
|||
|
||||
;;; Code:
|
||||
(require 'biome-api)
|
||||
(require 'biome-multi)
|
||||
(require 'biome-query)
|
||||
(require 'biome-grid)
|
||||
|
||||
|
|
@ -73,6 +74,18 @@ API."
|
|||
;; previous invocation of `biome'
|
||||
(biome-query--section-open (alist-get :name biome-query-current)))
|
||||
|
||||
(defun biome-multi ()
|
||||
"Run multiple queries to Open Meteo and join the results."
|
||||
(interactive)
|
||||
(funcall-interactively
|
||||
#'biome-multi-query
|
||||
(lambda (query)
|
||||
(biome-api-get-multiple
|
||||
query
|
||||
(lambda (queries results)
|
||||
(let ((merged (biome-multi--merge queries results)))
|
||||
(funcall biome-frontend (nth 0 merged) (nth 1 merged))))))))
|
||||
|
||||
(defmacro biome-def-preset (name params)
|
||||
"Declare a query preset.
|
||||
|
||||
|
|
@ -90,5 +103,19 @@ PARAMS as query."
|
|||
(setq biome-query-current ',params)
|
||||
(biome-query--section-open (alist-get :name ',params))))
|
||||
|
||||
(defmacro biome-def-multi-preset (name params)
|
||||
"Declare a multi-query preset.
|
||||
|
||||
NAME is the name of the target function. PARAMS is a form as defined
|
||||
by `biome-multi-query-current'.
|
||||
|
||||
This macro creates an interactive function that runs `biome-multi' with
|
||||
PARAMS as query."
|
||||
(declare (indent 1))
|
||||
`(defun ,name ()
|
||||
(interactive)
|
||||
(setq biome-multi-query-current ',params)
|
||||
(call-interactively #'biome-multi)))
|
||||
|
||||
(provide 'biome)
|
||||
;;; biome.el ends here
|
||||
|
|
|
|||
BIN
img/multi.png
Normal file
BIN
img/multi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
Loading…
Add table
Reference in a new issue