osm-codes / ws Goto Github PK
View Code? Open in Web Editor NEWGeneral WebServices of the OSM.codes
License: Apache License 2.0
General WebServices of the OSM.codes
License: Apache License 2.0
Conforme previsto em https://osm.codes/_foundations/2sbide_poster-GeoURI-v2.pdf o protocolo GeoURI foi extendido para comportar geocódigos. Na raiz, geo:{geocodigo}
, seriam os geocódigos iniciados por jurisdição, no geo:{tipo}:{geocodigo}
os demais tipos.
Agora temos geo:ghs:{codigo}
para Geohash e geo:olc:{codigo}
para PlusCodes, com todas as funções em https://github.com/osm-codes/WS/blob/main/src/lib-geoUriExt.sql
Consolidando solicitação semelhante em #36:
Usando a Expansão do protocolo GeoURI.
Por exemplo para a resolução de Geohash, https://osm.codes/geo:ghs:d2g69pmx
mas precisa decidir se resolve em célula da grade postal ou se mantém em Geohash. O natural por hora seria grade postal, como já faz com Geo URI.
O passo-a-passo da implantação de um geocódigo para o país é o seguinte:
As opções que surgem nesse conjunto de decisões deve ser contemplada pela nova biblioteca, ou seja, ela precisa ser generalizada para dar conta das alternativas.
PS: até o momento todos os países preferiram ficar com a curva-Z, não há demanda por Hilbert.
Em breve precisaremos resolver códigos ISO de 3 letras: alguns países pequenos, com "código de 2 letras nada haver", já adotam como canônico o de 3 letras, e alguns mais raros ainda não possuem representação de 2 letras. Incluir issue para conferir a situação do ISO 3 letras com https://datahub.io/core/country-codes
Conforme discussão votada e outras fontes, o PostgreSQL vai perder performance com FDW: se estão na mesma máquina, o melhor é uma base só com tudo.
(...) if you are setting up two databases on the same machine you probably want to make two schemas instead - in that case you don't need anything special to query across them.
O uso de optim.vw01full_jurisdiction_geom
em modo FDW mostrou o quanto a performance é comprometida.
(aguardar juntar mais requisitos)
textarea
) para acumular todos os geocódigos solicitados, ou cobertura solicitada pela URL.PS: esta issue é continuação de #13
Os Natural Codes foram formalizados em http://osm.codes/_foundations/art1.pdf
Os OSMcodes permitem usar qualqer base, inclusive base4h
PS: a expanção do padrão Geo URI para comportar geocódigos foi proposta em
https://inde.gov.br/images/inde/poster3/Expans%C3%A3o%20do%20protocolo%20GeoURI.pdf
Por exemplo no Uruguai temos esse ponto, do geocódigo postal UY-CO-ColoniaSacramento~4D8DM, e o sistema já sabe que a cidade é código 400, então deveria valer UY-400~4D8DM
, mas o link não funciona.
PS: o caso análogo em Bogotá funciona, https://osm.codes/CO-11001-Y88H2
Exemplo: BR-SP-SaoPaulo~CD existe e é resolvido pelo endpoint depois de ser validado pelo NGINX, nas BR-SP-SaoPaulo~CE nem sequer foi resolvido, o próprio NGINX descartou: sem maiores explicações, o usuário nem sabe que foi erro de sintaxe.
O retorno de código de erro, sempre que possível, deve ser delegado ao resolvedor de nomes, apenas o "alto nível do endpoint" fica a cargo do NGINX. Com isso, além de ser mais correto para a mensagem de erro, reduzimos a carga de CPU (por complexidade de regex) no NGINX... Portanto a "falsa responsabilidade" será considerado bug.
Analisando src/config.nginx temos:
location ~* "^/(urn|geo):lex:([a-z]{2}(;[a-z\.]+)?(;[a-z\.]+)?)\.json(/cover/base16h(1c)?)?$"
location ~* "^/(?:urn|geo):lex:[^/]+\.json(?:/cover/base16h(1c)?)?$"
api.f()
, demandam balanço entre a resolução genérica (com CASE
para demais funções) e especializada. Novamente, o ideal é não sobrecarregar nem a performance nem a manutenção do NGINX. Casos mais graves:
location ~* "^/geo:osmcodes:([A-Z]{2})~([0123456789BCDFGHJKLMNPQRSTUVWXYZ]+)(,[0123456789BCDFGHJKLMNPQRSTUVWXYZ]+){0,}\.json$"
para simples location ~* "(?i)^/geo:osmcodes:.+\.json$"
, e respectivo rewrite "(?i)^/geo:osmcodes:([A-Z]{2})~(.+)\.json$"
... Testar diversos casos porque o \.+
pode não se comportar conforme esperado.(?i)
é ignore-case, não adianta nada no rewrite sem usar ~*
antes no location.Dá issue osm-codes/BR_new#11:
Levar as issues específicas para cada país, por exemplo osm-codes/gridMap-draftPages#82 é exclusiva do Brasil.. Mas antes precisamos de NGINX enxuto como diretiva geral, e algum script de verificação de não-conflito. Ideal os scripts NGINX serem gerados por template Mustache e então o mesmo JSON que alimenta o Mustache-NGINX alimenta também um python de teste.
Dá issue #50:
Definir regrexes em CSV ou JSON. As regexes podem ser de dois tipos: genérica-global (no WS), e específica por sobrescrita (no país).
Cada país precisa do eu próprio gerador de NGINX a partir de CSV ou JSON (local!) que alimentará Mustaches.
Em outra issue já comentamos também que o ideal é cada país ter seu make_conf.yaml
para configuração do AFAcodes da jurisdição (incluindo projeção etc.), independente de ter sido automatizada ou não, permite gerar a documentação automatizamente e oferecer maior transparência quanto à definição do AFAcode.
É importante desmembrar ou simplesmente não trazer tudo para WS (WebService), visto que a temática é outra. Podemos manter tudo numa biblioteca só, Generalized-Geohash, ou quebrar em duas, uma só para grade e outra só para geocódigos (hcode). Os geocódigos nominais (prefixo de geocódigo postal) ficam cada qual na sua jurisdição.
Já podemos supor que em todos países a denominação "geocódigo científico" será associada à base16h e sua grade, sem outra opção... E que, quando usada, a base32 será relacionada apenas ao sistema misto (nominal+grade) de geocódigo postal.
Trazer para o novo projeto as respectivas PubLibs, que serão agora libs de um Schema, e demais libs:
A interface funcionava, mas alguns bugs foram introduzidos, precisam ser corrigidos antes de fecharem como v1.0.0.
Publicar HTML e Javascript mínimos que funcionem em harmonia com os webservices v1.0.0. Isso requer fechamento simultâneo dos padrões e dos repositórios que os implementam.
HZD,HZF,HZH,HZS,HZ7
.BR-SP-SaoPaulo
só cobre metade.Atributos a apresentar na popup da célula ou na sua descrição lateral:
Conferir qual está certo e solicitar correções se necessário.
SELECT wikidataid, wikidata_id, iso_a2, j.isolabel_ext, c.iso_n3
FROM countries c INNER jOIN optim.jurisdiction j ON j.osm_id=c.osm_id
where substr(wikidataid,2)::bigint!=wikidata_id;
wikidataid | wikidata_id | iso_a2 | isolabel_ext | iso_n3 |
---|---|---|---|---|
Q145 | 419 | PE | PE | 604 |
Q23792 | 219060 | PS | PS | 275 |
Q34497 | 192184 | SH | SH | 654 |
Q133888 | 408 | -99 | AU | 036 |
CREATE INDEX optim_jurisdiction_geom_idx1 ON optim.jurisdiction_geom USING gist (geom);
CREATE INDEX osm_coverage_geom_idx1 ON libosmcodes.coverage USING gist (geom);
CREATE INDEX osm_coverage_geom4326_idx1 ON libosmcodes.coverage USING gist (geom_srid4326);
Conferir todas as demais: todas as geometrias de optim precisam ser otimizadas com indices.
CUIDADO, DROP INDEX tem pegadinha em tabela com schema! Precisa desse SET mágico que o Guia não fala, e deixa totalmente não-amigável o SQL.
SET search_path ="optim";
DROP INDEX optim_jurisdiction_geom_idx1; -- demora!
SET search_path ="libosmcodes";
drop index osm_jurisdiction_geom4326;
drop index osm_jurisdiction_geom;
A tabela osm_city
da base DL03t_main pode ser renomeada para optim.jurisdiction_geom
para integrar os cores A4A e OSMC.
Agora possui tanto cidades como países (dados da tabela countries do Natural Earth).
Algumas decisões sobre escolha de grade dependem de parâmetros métricos da geometria da jurisdição, e estes podem ficar em cache (info
), tanto para reuso nesses cálculos como por seu valor informativo para o usuário final.
As grandezas relevantes são descritas pelos comments (ver com \d+
no pgsql):
Column | Type | Storage | Description |
---|---|---|---|
area_km2 | integer | plain | Area in square quilometers. |
area_sr | double precision | plain | Area in spheroradians. |
side_estim_km | double precision | plain | Estimating the side size of an equivalent-area square, in quilometers. |
side_estim_deg | double precision | plain | Estimating the side size of an equivalent-area square, in degrees. |
fat_deg_m | numeric | main | Average degree per meter convertion factor at the points of this area. |
elongation_factor | numeric | main | Factor between sides of an estimated rectangular box fitting the simplified geometry. |
O script abaixo precisa ser incluso nos procedimentos padrão de ingestão.
DROP VIEW IF EXISTS optim.vw03prepare_jurisdiction_metrics1;
CREATE VIEW optim.vw03prepare_jurisdiction_metrics1 AS
SELECT osm_id, isolabel_ext, round(area_m/1000000.0)::int as area_km2,
round( area_sr, 5)::float as area_sr,
round( SQRT(area_m)/1000.0, 1)::float as side_estim_km,
round( SQRT(area_sr), 4)::float as side_estim_deg,
round( SQRT(area_sr)/SQRT(area_m), 7 ) as fat_deg_m
FROM (
SELECT *, st_area(geom,true) area_m,
st_area(geom) area_sr
FROM optim.vw01full_jurisdiction_geom
WHERE geom IS NOT NULL
) t0
;
comment on view optim.vw03prepare_jurisdiction_metrics1 IS 'Prepare the standard geometry metrics.';
comment on column optim.vw03prepare_jurisdiction_metrics1.area_sr IS 'Area in spheroradians.';
comment on column optim.vw03prepare_jurisdiction_metrics1.side_estim_km IS 'Estimating the side size of an equivalent-area square, in quilometers.';
comment on column optim.vw03prepare_jurisdiction_metrics1.side_estim_deg IS 'Estimating the side size of an equivalent-area square, in degrees.';
comment on column optim.vw03prepare_jurisdiction_metrics1.fat_deg_m IS 'Average degree per meter convertion factor at the points of this area.';
-- Perigo!
UPDATE optim.jurisdiction
SET info = COALESCE (info,'{}'::JSONB) || (to_jsonb(t) - 'osm_id')
FROM (
SELECT osm_id, area_km2, area_sr, side_estim_km, side_estim_deg, fat_deg_m
FROM optim.vw03prepare_jurisdiction_metrics1
) t
WHERE t.osm_id=jurisdiction.osm_id
; -- UPDATE 7133
Tradicionalmente usamos uma "BBOX livre" (independente grade ou do sistema de coordenadas) para avaliar se a box será mais quadrada ou retangular. Todavia a BBOX será muito sensivel a geometrias "com braços" ou pequenos trechos alingados, não-representativos da área total do polígono. Um forma menos sensível de estimar o tamanho dos lados de um retângulo de área equivalente é através da função shapedescr_sizes, definida em https://gist.github.com/ppKrauss/3810651
As aplicações são diveras, por exemplo para avaliar o formato de quadras retangulares, ou estimar o grau de "alongamento" de polígonos em geral, Nos algoritmos de grade usaremos o fator de alongamento como recurso para decidir se a jurisdição precisa ou não fazer uso de gecódigos indexados.
DROP VIEW IF EXISTS optim.vw04prepare_jurisdiction_shapemetrics;
CREATE VIEW optim.vw04prepare_jurisdiction_shapemetrics AS
SELECT *, round( (rectang_factor_deg + 1.5*elongation_factor_deg)/2.5 , 2)::float AS elong_deg_mixfactor
FROM (
SELECT osm_id, isolabel_ext,
round(s[1]/fat_deg_m)/1000 as rectang_L_km,
round(s[2]/fat_deg_m)/1000 as rectang_H_km,
CASE
WHEN s[6]=0 THEN (round( ((s[1]-s[2])/side_estim_deg)/fat_deg_m )/1000.0)::int
ELSE -1
END AS elongation_factor_km,
CASE
WHEN s[6]=0 THEN round( (s[1]-s[2]) / side_estim_deg , 2)::float
ELSE -1
END AS elongation_factor_deg,
round( 0.95 + (diaglen_deg - side_estim_deg)/side_estim_deg , 3)::float as rectang_factor_deg
FROM (
SELECT *, (info->'side_estim_deg')::float AS side_estim_deg,
(info->'side_estim_km')::float AS side_estim_km,
(info->'fat_deg_m')::float AS fat_deg_m,
shapedescr_sizes( ST_SimplifyPreserveTopology(geom, (info->'side_estim_deg')::float/350.0) ) AS s,
-- ideal usar ao inves de side_estim a media (diaglen_deg+side_estim_deg)/2.0
ST_Length( ST_BoundingDiagonal(geom,true) ) as diaglen_deg
FROM optim.vw01full_jurisdiction_geom
WHERE geom is not null AND info?'side_estim_deg'
) t1
) t2;
-- check:
SELECT percentile_disc( array[0.2, 0.5, 0.8] ) WITHIN GROUP (ORDER BY elongation_factor_deg) as pctl_elongation_factor_deg ,
percentile_disc( array[0.2, 0.5, 0.8] ) WITHIN GROUP (ORDER BY rectang_factor_deg) as pctl_rectang_factor_deg,
percentile_disc( array[0.2, 0.5, 0.8] ) WITHIN GROUP (ORDER BY elong_deg_mixfactor) as pctl_mix
FROM optim.vw04prepare_jurisdiction_shapemetrics
WHERE isolabel_ext LIKE 'CO-%-%' AND elongation_factor_deg>0
; -- shape={1.5,2.02,2.63} | pctl_rectang={1.858,2.017,2.255} | pctl_mix={1.65,2.02,2.47}
-- Estamos adotando o mix, deu mais certo.
----- Perigo!
UPDATE optim.jurisdiction
SET info = COALESCE (info,'{}'::JSONB) || jsonb_build_object('elongation_factor',t.elong_deg_mixfactor)
FROM optim.vw04prepare_jurisdiction_shapemetrics t
WHERE t.osm_id=jurisdiction.osm_id AND t.elongation_factor_deg>0
; -- UPDATE 7115
Sugestão a testar:
(info->'elongation_factor')::float < 1.7
(info->'elongation_factor')::float >=2.5
, exceto se for muito pequenaIdentificadores de célula são universais, valem para qualquer país, pois justamente iniciam com os bits do código do país... Existe além da representação universal de células, a representação de "prefixos de cobertura", que não são identficadores de célula exatamente, mas indexadores para o algoritmo de encode/decode de códigos curtos e coberturas L0.
Como todo sistema de Generalized Geohash se base em dividir quadrados em quatro, todos os "blocos de bit" da célula precisam ser compatíveis com base4... Com algumas exceções. Blocos do identificafor de 64 bits, concatenados 3 blocos: <$lixo',
$pais,
$L0,
$celula_from_L0`>
$pais
: código ISO, supondo 1 a 999, mas codificados em 8 bits, tudo bem.$L0$celula_from_L0
: 4 bits (dígito base16) ou 5 bits (país optou priorizar base32) de L0 com o resto... Sim, não precisa L0 ser múltiplo de 4 mas precisa encaixar certo com o restante, já que só completam 10 bits a cada 2 dígitos base32 (e 10 é multiplo de 2 tudo bem).Ver issue já resolvida.
São apenas dois
Nas configurações NGINX, o destino default dos geocódigos nominais de jurisdições, é a interface de geocódigo postal. Ver em https://github.com/osm-codes/WS/blob/main/src/conf.nginx a mudança de /view
para /postal
.
Os casos gerais e de grade são gerenciados pela grade científica, /scientific
.
OSM ID of relations, ways and nodes. Examples:
osm.codes can redirect from abbreviated 1-letter form: relation/296621
=r/296621
; way/498698840
=w/498698840
; node/4900702088
=n/4900702088
.
Proposed endpoint: https://osm.codes/{letter}{digits}
.
NGINX locations expressions:
[rR]?([0-9]+)
to https://www.openstreetmap.org/relation/$1
[wW]([0-9]+)
to https://www.openstreetmap.org/way/$1
[nN]([0-9]+)
to https://www.openstreetmap.org/node/$1
See https://stackoverflow.com/q/27406188/287948 for non-redundant. Alternative:
location ~ ^/?([RrWwNn]?)([0-9]+)$ {
set $totype relation;
set $ck $1;
if ($ck = 'w') { set $totype way; }
if ($ck = 'W') { set $totype way; }
if ($ck = 'n') { set $totype node; }
if ($ck = 'N') { set $totype node; }
return 301 https://www.openstreetmap.org/$totype/$2;
}
Typical use: as alternative URL for https://www.wikidata.org/wiki/Property:P402 and others that need one-code (with no /
or -
separator). Default (with no letter) redirects to relation.
Tests:
Pending NGINX problem, see https://stackoverflow.com/q/75430954/287948
Na interface e nos recortes se faz necessária a adição do mar territorial. O polígono do "jurisdição marítima" pode ser obtido da subtração da jurisdição completa pela jurisdição continental. É importante que os polígonos recortados da grade L0 façam uso desse recorte. Sem isso a representação da grade recortada ainda estará errada (bug).
Exemplo na Colômbia:
NOTA: em geral o mar territorial está sob jurisdição da Marinha, portanto seria federal... Mas cada país pode ter a sua norma e resultar em jurisdições municipais de mar territorial. **Em caso de jurisdição federal não deve entrar no código postal (que é inerentente municipal), apenas na grade completa.
A definição das coberturas requer centro urbano, e nem sempre teremos a grade de densidade populacional.
As coordenadas de "ponto no município" da Wikidata apontam para o centro populacional do polígono.
Exemplo: CO-ANT-Sabaneta tem sua geometria em relation 1307270, que por sua vez traz o node do centro populacional.
Falta então descobrir como filtrar esses pontos para a nossa base. Eles diferem do centro geométrico.
[Futuro] Além das funções de encode/decode as bibliotecas de geocódigos em geral são munidas de operações de vizinhanha. No caso da curva de Morton, uma das mais importantes é a checagem do entorno.
Na biblioteca Javascript de movable-type.co.uk/scripts/geohash temos a função neighbours(geohash)
que retorna os 8 vizinhos doi Geohash do centro.
Tipicamente no SQL faremos SELECT * FROM t WHERE geocode IN neighbours(ref_geocode)
Existem desde as bibliotecas mais simples às mais refinadas. Exemplos extremos:
Documentar o complemento: em uma outra tabela (a de-para) existe uma extensão OsmCodes (convenção ainda arbitrária já que não é oficial do país) para isoLabel_ext, que são os quadrantes da cidade, C, N, S, etc. Em muitos casos vamos incluir esse 4o nivel de jurisdição isoLabel
Outra questão: o que o usuário quer dizer com "CO-ANT-Itagui", seria o polígono da cidade inteira ou o default, "CO-ANT-Itagui-C" ?
SOLUCAO: quando houver algo mais, como "CO-ANT-Itagui~123" ou apenas "CO-ANT-Itagui~", o canônico assoaciado será "CO-ANT-Itagui-C". Quando for só o nome, "CO-ANT-Itagui", então é a própria jurisdição.
E aí passamos a uma nova issue: os "defaults dinâmicos". Não precisa ser o "-C", cada jurisdição subdividida terá seu flag no banco de dados, coluna "is_subdiv_default boolean", indicando quem é default.
Pendente passar para pro git em https://github.com/osm-codes/WS/blob/main/src/nginx/osm.codes
e @crebollobr implementar HTTPS.
Para contemplar casos como https://osm-codes.github.io/Sfc4q/ precisamos do git-site.osm.codes
, por exemplo https://git-site.osm.codes/Sfc4q
server {
listen 80;
listen 443 ssl;
include /etc/nginx/ssl.conf;
server_name git-site.osm.codes;
location ~ ^/?.+ {
rewrite
^/?(.*)$
/$1
break;
proxy_pass http://osm-codes.github.io;
}
} # \server
Tal como em /var/gits/_a4a/WS/src/config.nginx
deve haver um só /var/gits/_osmc/WS/src/config.nginx
. Quem faz o controle de versões é o git, não o humano.
PS: jamais poluir ou desestruturar o git master, usar git branching para o desenvolvimento.
See original proposal at https://github.com/OSMBrasil/semantic-bridge
Levar as issues específicas para cada país, por exemplo osm-codes/gridMap-draftPages#82 é exclusiva do Brasil.. Mas antes precisamos de NGINX enxuto como diretiva geral, e algum script de verificação de não-conflito. Ideal os scripts NGINX serem gerados por template Mustache e então o mesmo JSON que alimenta o Mustache-NGINX alimenta também um python de teste.
Dá issue osm-codes/BR_new#11:
Definir regrexes em CSV ou JSON. As regexes podem ser de dois tipos: genérica-global (no WS), e específica por sobrescrita (no país).
Cada país precisa do eu próprio gerador de NGINX a partir de CSV ou JSON (local!) que alimentará Mustaches.
Por exemplo esse buraco em CO-AMA-Leticia: ainda são 26, pode acrescentar mais um que seria o X6Q.
Está pendente o uso de ferramentas de validação.
Automatizar o cálculo de número mínimo de dígitos que o nome da jurisdição pode substituir (ex. Medelin só 1), e o número de dígitos da subdivisão direcional (Norte/Sul/etc), e encontrar quais são elas.
Por exemplo está com info
NULL, enquanto Campinas está completo:
select isolabel_ext, info->'elongation_factor' e
from optim.jurisdiction where isolabel_ext IN ('UY-RV-Masoller','BR-SP-Campinas') ;
-- isolabel_ext | e
-- --------------+----------
-- BR-SP-Campinas | 2.42
-- UY-RV-Masoller |
URGENTE documentar o workflow e não fazer ingestão sem garantir o processo completo.
PS: isso compromete a amostragem de teste e as heurísticas de geração de SVG.
É necessário generalizar e otimizar o sistema que estamos introduzindo com a tabela libgrid_co.de_para
:
https://github.com/osm-codes/CO_new/blob/main/src/gridLib.sql#L303
Separar dados terminológicos dos dados essenciais, já está previsto o uso de https://github.com/AddressForAll/geoterm
Usar bigint positivo composto de 4 partes:
2.1. Prefixo jurisd_base_id
com 10 ou mais bits.
2.2. Bits (3) para distinção de tipo de cobertura (00X
cobertura do país, 01X
cobertura municipal principal, 10X
cobertura municipal com overlap na primária, X
0 para cobertura full e 1 para cobertura parcial demandando geometria de exclusão).
2.3. Bits (14) para o contador de jurisdições municipais (adaptado ou direto do jurisd_local_id
oficial)
2.4. Bits válidos restantes para o sufixo com a representação bitstring da célula de cobertura conforme fixada pelo país.
Avaliar se muda performance deixando geometrias na mesma tabela ou em tabela separada. Seriam 2 geometrias, a de baixa (ex. submetida a ST_Simplify) e a de alta resolução, lembrando que "tipo full" dispensa geometria.
Resta esboçar os algoritmos de resolução (decisão de qual jurisdição usar) e endode/decode, para confirmar se a estrutura de dados proposta garante maior performance.
Exemplo da fronteira Brasil-Colômbia:
Alternativas para o algoritmo de discriminação, ou seja, para decidir se o encode será através da grade de um país ou outro.
Faz uso da geometria do país inteiro: dado um ponto, determina em qual país está contido. Usa simples WHERE country_geom && pt_geom AND ST_CONTAINS(country_geom,pt_geom)
.
Vantagens:
A busca é feita por coberturas, cada uma delas contém a interseção entre país e a célula L0, de modo que podemos fazer a mesma busca WHERE country_geom && pt_geom AND ST_CONTAINS(country_geom,pt_geom)
. Neste caso o PostgreSQL pode de fato ter menos trabalho, fica só com a BBOX da primeira comparação quando é totalmente distinto dos demais... Como as grades L0 possuem origem em uma geometria do espaço projetado, não possuirá uma representação tão simples. Por isso a performance ainda será baixa e precisa ser comparada com o caso anterior, talvez não compense.
PS: células de cobertura menores reduzem o índice de sobreposição. A sugestão é que, tal como nas coberturas municipais, as de nível zero também ofereçam discriminação... Se isso realmente representa otimização, há que se levar em conta nas decisões individuais de cada país, não seria mais uma decisão totalmente neutra.
A conversão de coordenadas LatLong em Geohash de 1 ou 2 bytes é rápida, e é tudo o que precisamos para montar um algoritmo similar ao "força bruta distribuída", porém agora orientado a uma grade independente da projeção. Requer duas partes:
Idealmente pode seria otimizado para indicar a célula L0 onde se de o match, mas com probabilidade não muito maior do que 50%.
Existe alguma implementação de Quadree, https://doxygen.postgresql.org/spgquadtreeproc_8c.html
aparentemente aquela descrita em https://www.cs.purdue.edu/spgist/publications.xml
Criar um método geral para levar informações para a grade, ou seja, para oferecer na grade 1 Kim de cada país um resumo de informações úteis da célula: população estimada, temperatura média anual, insolação média anual, número de endereços no addressForAll, número de vias que cruzam a célula, rótulos das cidades, etc.
Conforme #23 podemos usar RT_ST_AsRaster no nosso servidor.
A forma mais simples e "leve" para levar os atributos de um layer para a grade cienífica, aparentemente, é usando os recursos raster do PostGIS.
Testar e aprimorar, ou oferecer alternativas para comparar performance.
Eleger "tamanho de bloco", pois a conversão se dará por blocos, não dá para rodar o país inteiro de uma vez.
Projeta num raster de alta resolução ... https://postgis.net/docs/RT_ST_AsRaster.html
Devolve o raster para um polígono obtendo o valor por célula ... http://www.postgis.net/docs/RT_ST_SummaryStats.html
A cada layer (por exemplo mosaico de jurisdições) ao invés de armazenar sistematicamente cada valor em cada célula, agregar valores iguais (cobertura uniforme) na célula-mãe. NULLs ou grandes areas homogêneas se beneficiarão desse recurso. O valor da célula-mãe continua sendo tão exato quanto a união das filhas. É a estratégia quadtree.
Convenção:
CREATE TABLE osmc.jurisdiction_grid_info (
gid bigint NOT NULL PRIMARY KEY, -- célula iniciada pelo ID de 16 bits da jurisdição, sobram 48 bits para a célula
info JSONb NOT NULL, -- key-value onde key é um rótulo curto iniciado por "_" quando for cache das filhas
is_info_root boolean -- indica que info contém alguma informação bruta, não-cache.
);
o gid
perde 1 bit de sinal e 1 bit de "hidden bit" para garantir a hierarquia, mas como a jurisdição só necessita da ordem de 10 bits, podemos incluir esses dois como valores constantes da máscara. Em seguida todos os bits são da célula.
No caso do Brasil são 40 níveis, de modo que 40 bits são efetivamente utilizáveis, mas na prática nos limitaremos ao nível 10, ou seja, 20 bits, correspondendo às células de 1024 metros de lado.
Os valores info precisam ser baseados em rótulos curtos para não ocupar muito espaço, por exemplo a população contida na célula seria pop
, ou, em caso de sumarização das filhas, _pop
. Quando uma célula menor não tem o valor deve-se exaurir a busca pelas células mães: essa "economia de espaço em disco" pode ser inúfil quando a célula já existe, mas pode economizar milhares de registros quando o número de layers com informação homogênea for grande. Aos poucos, se houver recurso em disco, a estratégia poderá ser dispensada... Ou não, otimizamos a busca por células-mãe. Uma das estratégias para reduzir esse custo é armazenar dados apenas em níveis válidos da base 16, reduzindo para 1/4 o número de verificações. No caso de 1 kim, que tem 7 dígitos (mas o primeiro é degenerado em 2) seriam 5 buscas até a grade de cobertura, que em geral não é root.
Com essa estratégia alguns municípios, tipicamente os alongados, terão o "primeiro dígito recodificado" depois do prefixo nominal. Exemplificando com Bogotá, que de outro modo não gastaria 1 ou dígitos a mais.
A "cobertura Bogotá" é fornada pelo conjunto de todas as células de um certo nível L (ou número de dígitos D) com interseção na geometria da cidade. Na ilustração elas correspondem às células com alguma parte em rosa.
O algoritmo que decide quais níveis D e D+1 usar para a cobertura, todavia, pode não ser tão simples. Aqui uma sugestão:
Conferir a cobertura da grade maior ou igual side_estim
(Bogota tem 40.6 km), que nesse caso é a cobertura com células com lado médio de 46.3 km. O resultado é uma cobertura de 6 células, ajustável para 4: HX
, HW
, HT
e HS
.
Então podemos dizer que existe uma cobertura em D dígitos, e, como todas as células possuem prefixo H, podemos dizer que "no nível D o nome seria traduzido para H
", se nada de melhor for conseguido.
PS1: pode acontecer de serem necessárias mais células de cobertura, o limite sempre será 32.
PS2: uma alternativa à análise geométrica da cobertura (ver funções geohash_cover_list() e outras) é obter uma amostragem de pontos e identificar o prefixo comum com hcodes_common_prefix... A desvantagem é que o risco de falhar quando a grade está em ponto de descontinuidade e só uma pequena porção passa por lá (e não seria amostrada). O algoritmo de cobertura por outro lado já foi bem testado e é confiável.
Como 4 é muito pouco (a checagem heurística pode ser 4 < 10), buscamos recodificar: quais células da cobertura de D dígitos devem ter sua subgrade adotada como cobertura?
A célula que contém o o ponto fornecido pela Wikidata como referência para o centro urbano da cidade é um bom ponto de partida (outra opção é a contagem de endereços em cada célula). A célula é HX
, e sua subgrade HX*
apresenta como cobertura HX1
, HX4
, HX5
, HXJ
, HX6
, HX7
, HXL
, ... HXU
, totalizando 12 células com interseção em Bogotá.
Das 4 células H*
, uma, HX
, foi expandida para 12 células, totalizando 12+3=15 células... Ainda é pouco.
2.1. Repetimos o processo enquanto o total na cobertura não exceder 31 (32 menos um de reserva). Claro, se terminar, como já passou dos 10 diremos que "está bom". Se não terminar significa que algumas células da cobertura H*
vão ficar: é o que ocorreu, depois de incluirmos HW
ainda sombrou, mas com HT
já não daria. Foram adicionadas HW1
, HW3
, HW4
, HW5
, HW6
, HW7
, ..., HWU
, ou seja mais 12 células. Agora o total é 15-1+12 = 26 células de cobertura.
PS: esse total de 26 células poderia ser "ajustado" para 22 ou 24... Uma das decisões de projeto é permitir ou não esse segundo nível de ajuste. Como dois já foram comprometidos no ajuste de HT
e HS
não seria probelma. Com essa decisão em mãos saberemos se dá ou não para recodificar HT
(e avaliar HMP
para ser ajustado para grade menor).
Lembrete:
O ajuste e a recodificação em mais detalhes:
Ajustar: criam-se entradas na "tabela de-para" das células órfans (ilustradas em amarelo).
Por exemplo a posição real HMP
vai ser simbolizada por HTP
, ou seja, conversão HTP
→HMP
contextualizado; da mesma forma as outras 5 células, HSN
→HLN
, HSP
→HLP
, HSQ
→HLQ
, HSR
→HLR
, HSX
→HLX
.
O ajuste não reduz o número de dígitos, mas reduz a quantidade de geocódigos consumidos, permitindo ou o encaixe direto (ver Sabaneta abaixo) ou a economia necessária para mais itens serem recodificados.
Recodificar: Por exemplo HS
=0
, HT
=1
, HW5
=3
, HW4
=4
, HW1
=5,
HW7=
6,
HW6=
7,
HW3=
8`, ... Mas seguindo a ordem dos IDs, de modo a ser facilmente reprodutivel. No exemplo a cobertura de Bogotá requer 25 células, restando ainda uma reserva de 7 células para eventuais modificações territoriais.
PS: reparar que tanto no geocódigo curto ajustado como no curto recodificado perdemos a compatibilidade com o geocódigo absoluto nesse primeiro dígito depois do ~
.
Ilustrando o algoritmo com Geohashes e usando BR-SP-SaoPaulo:
Ilustrando o "ajuste" com CO-ANT-Sabaneta, mas nesse caso não há necessidade de recodificar a cobertura.
Por exemplo CO-ANT-Sabaneta~4
é um primeiro dígito natural, equivalente ao geocódigo absoluto CO~8UR4
. Foi escolhido entre os demais por ter maior área de interseção. Por outro lado CO-ANT-Sabaneta~Q
precisa ser ajustado para CO~8UPQ
, o prefixo ajustado muda.
Nota sobre a sintaxe do código. Por haver essa perda de compatibilidade podemos convencionar que o primeiro dígíto depois do ~
no geocódigo curto é especial, e separa-lo sempre por um ponto. Exemplos CO-Sabaneta~4.123
, CO-Sabaneta~Q.456
. Outra estratégia seria isolar com outra sintaxe, já que pontos seriam livres. Ex. CO-Sabaneta~4~123
, CO-Sabaneta~Q~456
. A rigor não queremos juntar à parte nominal (ex. CO-Sabaneta-4~123
) pois não é uma subjurisdição.
Quanto menor o polígono maior a probabilidade de encaixe na célula... Mas qual o limite de ocupação do polígono? Vale a pena testar por exemplo com um polígono que ocupa 80% da área da célula?
A experiência abaixo mostra que talvez 90% seja realmente inútil, mas a área de CO-CAU-Florencia, de 57 km2, cobriria 85% dos 67 km2 da célula, e foi por pouco que não deu certo:
Comparando com Sabaneta, percebemos outro aspecto, sobre os pedaços (subcélulas) ajustados:
no caso de Sabaneta cada pedaço ajustado tem sua própria subcélula.
Exemplo: a subgrade de CO-Sabaneta~P
conteria apenas CO-8UPP*
, assim como CO-Sabaneta~R
apenas CO-8UPR*
. A exclusão também funciona, o absoluto CO-8UPRM
não faz parte de Sabaneta, assim como CO-Sabaneta~RM
não é válido.
no caso de Florencia alguns pedaços ajustados compartilhariam sua subcélula com outros... Isso não criaria um conflito de referência?
Vejamos a célula Florencia~P
que estaria mapeando CO-NM5P
e CO-NM4P
para o mesmo P
... E as subcélulas, como por exemplo, Florencia~P2
, não teriam mais garantia de correspondência com o código absoluto. A exclusão deixa de funcionar.
Trata-se de uma decisão de projeto: permitir "apenas cada pedaço ajustado a sua própria subcélula" ou não. Quando não, o algoritmo será mais complexo e exclusão (e portanto a sua correspondência com código absoluto) deixa de funcionar.
Usando a Expansão do protocolo GeoURI.
Por exemplo para a resolução de Geohash, https://osm.codes/geo:ghs:d2g69pmx
mas precisa decidir se resolve em célula da grade postal ou se mantém em Geohash. O natural por hora seria grade postal, como já faz com Geo URI.
Segundo https://support.dnsimple.com/articles/differences-between-a-cname-alias-url/
General rules: Use an A record if you manage which IP addresses are assigned to a particular machine, or if the IP are fixed (this is the most common case). Use a CNAME record if you want to alias one name to another name, and you don't need other records (such as MX records for emails) for the same name.
Temos usado a maioria como CNAME, mas o default da Digital Ocean é A, e alguns casos fizemos com A. Parece que a performance vai ser (desprezivelmente) melhor com A, mas supondo que o IP é de fato o ultimo passo.
Está disponível para o ambiente de teste, https://wiki.addressforall.org/doc/osmc:Conven%C3%A7%C3%B5es/Coberturas_municipais/Algoritmos
Se homologado, passaremos do Teste para a Produção.
Os core-datasets do OSMcodes são aqueles que configuram cada jurisdição e estruturam os demais dados. Atualmente residem no schema optim.
Cada vez que atualizamos, reconfiguramos ou inserimos uma nova jurisdição, precisamos conferir se não fizemos alguma bobagem, ou seja, precisamos validar os novos core-datasets. Certos algorimos de validação ajudam a filtrar dados incorretos, outros ajudam apenas a destacar suspeitas de falha:
filtros de validação: depois de carregados os dados em tabela intermediária, o próprio INSERT pode fazer essa validação. Um bom exemplo é o INSERT de novas geometrias no Digital-guard, através da função any_load()
, que armazena em separado os registros rejeitados e indica o erro detectado.
relatórios de validação: heurísticas que permitem destacar suspeitas de falha, e alertar sobre erros menores.
Criar uma VIEW para cada tipo de "relatório de validação" e, no caso de filtros, uma função de teste para cada tipo.
.. reduzir o nome a um nome Lex, as letras da abreviação precisam ser letras do nome Lex, e estar na mesma ordem...
... o número de células não pode exceder a base (32 ou 16); a interseção entre município e células precisa ter mesma área que o municíooo. Ver também #33
.. No caso do Uruguai não é obrigatório que todo estado seja um mosaico de municípios...
Originalmente as jurisdições vinham de datasets controlados como http://datasets.ok.org.br/city-codes
Agora cabe o controle em OSM-codes (não seria Digital-guard pois fazemos uso de multiplas fontes - aí sim cada fonte com seu guard).
Os datasets de jurisdiction podem ser construídos com base em
A consolidação das fontes é feita pelo PostgreSQL conforme heurística de confiabilidade. Relatórios estatísticos podem sugerir demandas por revisão humana.
Não existem fontes para a maior parte das abreviações, serão construídas por iniciativa da OSMcodes. Requer amadurecimento com a comunidade e legitimação com órgãos governamentais, longo processo, por isso demanda por tabelas de controle. Ver também https://github.com/AddressForAll/geoterm
CREATE TABLE optim.jurisdiction_abbrev_ref (
abbrevref_id int PRIMARY KEY,
name text not null,
info jsonb not null
);
CREATE TABLE optim.jurisdiction_abbrev_option (
selected boolean not null default false,
abbrevref_id int not null REFERENCES optim.jurisdiction_abbrev_ref,
isolabel_ext text not null,
abbrev text not null,
insert_date date not null default now(),
PRIMARY KEY (abbrevref_id,isolabel_ext,insert_date)
);
Pontos pink bem destacados e sem confusão com o "pin do click". A "grade de pontos" auxilia na decisão de atribuição de endereço.
Podemos gerar imagens pesadas no filesystem do server e realizar processamento on-the-fly, de imagens leves. O caso típico seria utilizando PNG com transparência, que pode ser mais simples e mais leve do que GeoJSON em aplicações que não exisgem maior precisão. O problema em geral vai ser a escolha de um fundo adequado... Em ilustrações didáticas mostrando as variações de uso pode ser útil, pois o fundo é fixo.
Conforme guia PostgREST e dicas PostGIS e guia RT_ST_AsRaster, podemos servir imagens!
CREATE EXTENSION IF NOT EXISTS postgis_raster;
SET postgis.gdal_enabled_drivers = 'ENABLE_ALL'; -- para PNG e JPG outputs
create table api.lixo as
SELECT 1 as gid, ST_AsPNG( ST_AsRaster(
ST_Buffer(ST_GeomFromText('POLYGON((-30 40, -20 30, -25 20, -23 10, -30 40))', 4326), 50),
100, -- width
100, -- height
'8BUI', -- pixeltype
118 -- value
) );
curl "http://osm.codes/_sql/lixo?select=st_aspng" -H "Accept: application/octet-stream" > lixo.png
# Opcional: sobrepor imagem geográfica com fundo branco ou tile system
convert background_tile.png lixo.png -append /tmp/result-tileLixo.png
location /_sql.img {
rewrite /_sql.bin/(.*) /$1 break;
proxy_set_header Accept 'application/octet-stream';
proxy_pass http://127.0.0.1:3103;
}
Falta decidir se queremos _sql.bin
ou _sql.img
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>title</title>
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
</head>
<body>
<h1>Imagem gerada on-fly!</h1>
<p>Gerado por <tt>http://osm.codes/_sql.img/lixo?select=st_aspng</tt>.</p>
<img src="http://osm.codes/_sql.img/lixo?select=st_aspng">
</body>
</html>
Cabo Verde (CV
) oferece dados abertos em seu https://codigopostal.cv de modo que, apesar de não oferecerem resolução por grade, por ser um país pequeno pode ter seu polígonos incorporados ao nosso banco de dados. Historicamente foi o primeiro país a pleitear e quase oficializar o uso do PlusCodes, posteriormente desistindo por considerar a resolução dos nomes de local fechada, e sem respeitar os padrões governamentais.
A Irlanda (EI
) foi talvez o primeiro país a oferecer um "código postal de grão-fino", que leva até a porta de casa (código do lote). Ver Eircode na Wikipedia. Infelizmente não é um algoritmo ou banco de dados aberto. Durante a "licitação pública" várias alternativas foram ofertadas, mas nenhuma delas parecia plenamente eficiente e bem fundamentada. O histórico tumultuado sugere a necessidade prévia de um padrão de referência, como o DNGS.
/CV-$code
, ex. CV-5116-002
; e /EI-$code
, ex. EI-D02.AF30
.CV
podemos vir a oferecer os polígonos. No caso de EI
, parece que não recebem links, a busca precisa partir da interface própria.Apenas os polígonos podem ser resolvidos, em ambos os casos não há como "chegar na porta de casa". Resolução parcial ou total do geocódigo:
CV, evidências da tentativa de usar PlusCodes:
CV, evidências da data de inicio do Novo Código Postal:
EI, evidências do uso vigente:
EI, críticas:
Fonte da box de busca de Eircode:
<form role="form" ng-submit="searchForm.$valid && searchEircode()" name="searchForm" novalidate="" class="ng-pristine ng-invalid ng-invalid-required ng-valid-minlength">
<div class="input-group">
<input placeholder="Enter a full address or Eircode" class="form-control ng-pristine ng-invalid ng-invalid-required ng-valid-minlength ng-touched" ng-class="{formcontrolRequired:searchForm.$submitted && !searchForm.$valid || InvalidEircode}" ng-model="searchQuery" name="searchQuery" required="" autocomplete="off" spellcheck="false" typeahead="options.displayName for options in getAutoCompleteAddresses($viewValue)" typeahead-on-select="onSelect($item, $model, $label)" typeahead-focus-first="true" focus="" ng-minlength="3" search-bar="" aria-autocomplete="list" aria-expanded="false" aria-owns="typeahead-76-2900"><ul class="dropdown-menu ng-isolate-scope ng-hide" ng-show="isOpen()" ng-style="{top: position.top+'px', left: position.left+'px'}" style="display: block;;display: block;" role="listbox" aria-hidden="true" typeahead-popup="" id="typeahead-76-2900" matches="matches" active="activeIdx" select="select(activeIdx)" query="query" position="position">
<!-- ngRepeat: match in matches track by $index -->
</ul>
<span class="input-group-btn" ng-class="{current:loading}">
<input type="submit" value="Search" class="btn btn-default btn-search-bar" ng-disabled="loading">
<div id="floatingCirclesG">... </div>
</span> </div>
<!-- ngIf: searchForm.$submitted -->
<div class="error-message ng-binding ng-hide" ng-show="InvalidEircode">
</div>
</form>
Pontos e lotes com pelo menos um, nome de rua e/ou numeração predial, distinguindo dados inéditos de não-inéditos, e exportando para shapfile
No banco de dados são armazenadas as geometrias resultantes da intersecção, então não há custo maior em se devolver ao usuário a geometria correta da cobertura nominal, tal qual fazemos uso na discriminação.
Na interface de testes, selecionar o level conforme a quantidade de dígitos do menor prefixo de cobertura mais um. Lever em conta a diferença na quantidade de dígitos e a correspondência com o level em cada uma das bases: postal e cientifica.
O tema é tratado entre os matemáticos como subpavimentação... Dada uma geometria X, temos duas aproximações, X⁻ e X⁺, tais que X⁻ ⊂ X ⊂ X⁺.
Precisamos de duas funções neste projeto:
A cobertura interior, ou seja, X⁻, e portanto uma aproximação de subpavimentação conforme os limites de grade ou de número de células imposto. Na ilustração é a parte vermelha.
A cobertura com borda, ou seja, X⁺. Na ilustração a união da parte vermelha com a amarela, onde a parte amarela é a aproximação de borda.
Outra abordagem, a Morfologia Matemática, trata sempre da aproximação de X como ponto de partida: X⁺ é a sua dilatação e X⁻ a sua erosão, mas existem centenas de maneiras diferentes para se erodir ou dilatar, ver livro em portugues. No PostGIS temos ST_Buffer positivo e negativo como recurso para depois discretizar na grade, quando um controle métrico for necessário.
Ver "region cover" (de fato esse é o nome mais popular para a funcionalidade) em s2.sidewalklabs.com/regioncoverer ou S2 Covering Examples.
A interseção pode ser uma boa referência:
interseção da borda com grade fixa de maior resolução: pode depois ser otimizada com substituição das células interiores por grades de menor resolução. Pode retornar a borda, o interior X⁻ ou X⁺.
interseção da borda com espeçura fixa: tomando-se um buffer métrico da linha de borda podemos medir a área de interseção de modo a selecionar mais corretamente a resolução mais grosseira, minimizando a quantidade de células que comporá a aproximação de borda.
interseção com tamanhos variávies: usar o número de células como parâmetro. A função buscaria a menor área de não-interseção, resultando em X⁺.
Listagem das pendências em vw_lixo_summary_jurisdiction, conforme https://github.com/osm-codes/WS/blob/main/src/country_sample_function.sql Falta ajustar ao esquema optim.
A listagem atual que nos fornece a situação atual e pendências. Alguns países já possuem git no Digital-Guard, portanto podemos iniciar:
isolabel | jurisd_base_id | n_level2 | n_level3 |
---|---|---|---|
BR | 76 | 27 | 5570 |
CL | 152 | 16 | 0 |
CO | 170 | 32 | 1115 |
EC | 218 | 24 | 0 |
PE | 604 | 26 | 1 |
VE | 862 | 25 | 0 |
Ver também AR, Uruguai, etc.
Para simular PlusCodes também registraremos nomes de vilas rurais, por exemplo CO-ANT-Medelin-Morritos, com mais de uma letra. Não são canônicos, são sempre traduzidos para um prefixo, traduzindo-se por ex. para CO-ANT-Medelin~12 , mas será um overhead na resolução de nomes que precisaremos avaliar como tratar.
Nesta issue listar na forma de checklist as pendências que vão sendo resolvidas conforme possível
O desenvolvimento do back-end depende de algum front-end para que testes e visualização humana possam ser realizados. Por isso acabaram sendo projetados dois back-ends:
interface de teste: também apelidada ou interface Leaflet. por ser mais simples e do conhecimento da equipe back-end, foi a primeira a ser desenvolvida.
terá uso secundário, sua principal função é a prototipação (prova de conceito) e o preparo de imagens ilustrativas.
interface de produção: também apelidada ou interface ReactJS. É a definitiva, aguarda implementação do site-v2. Deve atender a todos os requisitos de visualização pública. Simplicidade, leveza e eficiência serão solicitados.
As issues deste git, sobre interface, devem especificar se teste ou produção, pois terão requisitos distintos.
A interface teste vai permanecer na raiz do domínio osm.codes até que a interface de produção seja entregue em condições de substituir.
A interface teste deverá migrar para o domínio test.osm.codes depois do lançamento (enquanto isso não acontece, redireciona para osm.codes). Permanecerá lá ainda por um tempo, até que se tenha fôlego para investir em "visualizações extra" (controle complexos para gerar ilustrações) no React.
A finalidade principal dos geocódigos curtos é suprir a demanda nacional por códigos postais eficientes. A grade base32 em si não é um objetivo, inclusive a recomendação para uso científico e a base 16h.
Em ambos os casos, código postal e científico, o foco não é o desenho ou navegação arbirários, mas o uso de um contexto como ponto de partida: a interface de generalized-geohash explorer não precisa partir do globo, mas sim de um país ou um prefixo dentro de um país.
O prefixo e o tipo de geocódigo (postal ou científico) é que estabelecem a interface, antes do usuário definir o prefixo não faz sentido mostrar algo, ele precisa escolher o que quer... E sempre será contextual. Todas as interfaces precisam ser orientadas ao contexto e não ao globo.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.