вторник, 6 декабря 2011 г.

Пространственная фильтрация при создании PDF документа в геосервере

Для создания PDF документа в геосервере я использую плагин geoserver-2.1-SNAPSHOT-printing-plugin. Подробнее о его применении описано в Руководстве пользователя. Я формирую запрос:
http://tomcat:8080/geoserver/pdf/print.pdf?spec={
    "units":"m",
    "srs":"EPSG:60000",
    "layout":"A4 landscape",
    "dpi":100,
    "header":"2 rayon",
    "footer":"",
    "layers": [{
        "baseURL":"http://tomcat:8080/geoserver/wms",
        "opacity":1,
        "singleTile":true,
        "type":"WMS",
        "layers":["egko","net_rayon"],
        "format":"image/png",
        "styles":["",""]
    }],
    "pages":[{"bbox":[1009,4821,7336,12355]}]
}
и получаю ответ
На карте включено два слоя: egko и net_rayon. Мне требуется убрать все объекты с карты, которые расположены за пределами полигона в слое net_rayon, имеющего атрибут name со значением 2. Для этого обычно используют CQL фильтрацию. К сожалению, примера использования такой фильтрации я не нашёл. И в документации Mapfish print module это не описано.
Экспериментально-интуитивным методом я решил эту задачу. Я добавил параметр customParams, содержащий параметр cql_filter. Строка значения этого параметра содержит два фильтра (по одному на каждый слой), разделённые точкой с запятой. Первый слой фильтруется по пересечению с объектом второго слоя, имеющим определённое значение атрибута. Второй слой фильтруется по этому же значению атрибута. Новый запрос выглядит так:
http://tomcat:8080/geoserver/pdf/print.pdf?spec={
    "units":"m",
    "srs":"EPSG:60000",
    "layout":"A4 landscape",
    "dpi":100,
    "header":"2 rayon",
    "footer":"",
    "layers": [{
        "baseURL":"http://tomcat:8080/geoserver/wms",
        "opacity":1,
        "customParams": {
            "cql_filter":"INTERSECTS(geom,querySingle('net_rayon','geom','name=2'));name=2"
        },
        "singleTile":true,
        "type":"WMS",
        "layers":["egko","net_rayon"],
        "format":"image/png",
        "styles":["",""]
    }],
    "pages":[{"bbox":[1009,4821,7336,12355]}]
}
В результате возвращается PDF с отфильтрованными объектами:
Протяжённые объекты (реки, дороги) выходят далеко за пределы выбранного полигона. Такие крупные объекты можно преобразовать в несколько меньших, разделив их по границам полигонов слоя net_rayon. В настоящем примере использована функция querySingle, входящая в плагин querylayer-2.1.0.jar.

понедельник, 21 ноября 2011 г.

Вызов javascript функции в Firefox из Windows приложения

Разрабатываются Windows приложение и Web приложение. Они выполняются одновременно. Возникла задача передать данные из первого приложения во второе и выполнить там javascript функцию с переданными данными. Для IE всё настолько тривиально, что здесь даже не стоит обсуждать; сложнее оказалось для Mozilla Firefox.
Я стремился следовать принципу KISS при решении этой задачи. Весьма полезными оказались следующие ресурсы: Предлагаемый способ основан на создании в Firefox невидимого окна для приёма сообщений. Это окно можно будет найти по имени класса и имени окна (я использую "Modus RP Class" и "Modus RP"). Для упрощения примера я буду передавать строку, но можно передать любую структуру. Также для упрощения в Web приложении будет вызываться функция window.alert(). В Delphi 2010 для отправки сообщения я на форму добавил кнопку TButton и обработчик события OnClick:
procedure TForm1.Button1Click(Sender: TObject);
var
  s: String;
  copyData: TCopyDataStruct;
  targetHandle: HWND;
begin
  s := 'Текущее время: ' + DateTimeToStr(Now());
  copyData.lpData := PChar(s);
  copyData.cbData := SizeOf(Char) * (Length(s) + 1);
  targetHandle := FindWindow('Modus RP Class', 'Modus RP');
  if targetHandle <> 0 then
    SendMessage(targetHandle, WM_COPYDATA, Handle, Longint(@copyData));
end;
Теперь привожу исходный код страницы HTML:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>Call JavaScript Function</title>
    <script type="text/javascript" language="javascript">
    
    function getBrowserName() {
        var browserName = "";
        var ua = navigator.userAgent.toLowerCase();
        if ( ua.indexOf("opera") != -1 ) 
            browserName = "opera";
        else if ( ua.indexOf("msie") != -1 ) 
            browserName = "msie";
        else if ( ua.indexOf("safari") != -1 ) 
            browserName = "safari";
        else if ( ua.indexOf("mozilla") != -1 ) 
            if ( ua.indexOf("firefox") != -1 ) 
                browserName = "firefox";
            else 
                browserName = "mozilla";
        return browserName;
    };
    
    if (getBrowserName() == "firefox") {    
        try {
            netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
            Components.utils.import("resource://gre/modules/ctypes.jsm");

            var user32 = ctypes.open("user32.dll");
            var kernel32 = ctypes.open("kernel32.dll");

            const HWND_MESSAGE = -3; 
            const WM_COPYDATA = 74;

            var lpszClassName = ctypes.char.array()("Modus RP Class");    
            var lpszWindowName = ctypes.char.array()("Modus RP");
            
            const COPYDATASTRUCT = ctypes.StructType("COPYDATASTRUCT",
                [ 
                  {dwData: ctypes.uintptr_t},
                  {cbData: ctypes.uint32_t},
                  {lpData: ctypes.voidptr_t}
                ]);    

            var DefWindowProc = user32.declare("DefWindowProcA", ctypes.winapi_abi, ctypes.int,
                ctypes.voidptr_t, ctypes.int32_t, ctypes.int32_t, ctypes.int32_t);
                
            function windowProcJSCallback(hWnd, uMsg, wParam, lParam) {
              if (uMsg == WM_COPYDATA) {
                var CD = COPYDATASTRUCT.ptr(lParam).contents;
                var s = ctypes.cast(CD.lpData, ctypes.jschar.ptr).readString();
                alert(s);
                return ctypes.int(0);
              } else 
                return DefWindowProc(hWnd, uMsg, wParam, lParam);
            }
            
            var WindowProcType = ctypes.FunctionType(ctypes.stdcall_abi, ctypes.int,
                [ctypes.voidptr_t, ctypes.int32_t, ctypes.int32_t, ctypes.int32_t]).ptr;
            var WindowProcPointer = WindowProcType(windowProcJSCallback);
            var WNDCLASS = ctypes.StructType("WNDCLASS",
                [
                  { style: ctypes.uint32_t },
                  { lpfnWndProc: WindowProcType }, 
                  { cbClsExtra: ctypes.int32_t },
                  { cbWndExtra: ctypes.int32_t },
                  { hInstance: ctypes.voidptr_t },
                  { hIcon: ctypes.voidptr_t },
                  { hCursor: ctypes.voidptr_t },
                  { hbrBackground: ctypes.voidptr_t },
                  { lpszMenuName: ctypes.char.ptr },
                  { lpszClassName: ctypes.char.ptr }
                ]);
                
            var GetModuleHandle = kernel32.declare("GetModuleHandleA", ctypes.winapi_abi, 
                ctypes.voidptr_t, ctypes.char.ptr);    
            var hInstance = GetModuleHandle(null);    
                
            var wndclass = WNDCLASS();
            wndclass.hInstance = hInstance;
            wndclass.lpszClassName = lpszClassName;
            wndclass.lpfnWndProc = WindowProcPointer;   

            var RegisterClass = user32.declare("RegisterClassA", ctypes.winapi_abi, 
                ctypes.uint32_t, WNDCLASS.ptr);
            var UnregisterClass = user32.declare("UnregisterClassA", ctypes.winapi_abi, 
                ctypes.bool, ctypes.char.ptr, ctypes.voidptr_t);
            var DestroyWindow = user32.declare("DestroyWindow", ctypes.winapi_abi, 
                ctypes.bool, ctypes.voidptr_t);

            RegisterClass(wndclass.address());

            var CreateWindowEx = user32.declare("CreateWindowExA", ctypes.winapi_abi, 
                ctypes.voidptr_t, ctypes.int, ctypes.char.ptr, ctypes.char.ptr, ctypes.int, 
                ctypes.int, ctypes.int, ctypes.int, ctypes.int, ctypes.voidptr_t, 
                ctypes.voidptr_t, ctypes.voidptr_t, ctypes.voidptr_t);

            var win = CreateWindowEx(0, lpszClassName, lpszWindowName, 0, 0, 0, 0, 0, 
                ctypes.voidptr_t(HWND_MESSAGE), null, hInstance, null);    

            window.onunload = function () {
              if (win && !win.isNull()) {
                  DestroyWindow(win);
                  UnregisterClass(lpszClassName, hInstance);
                  user32.close();
                  kernel32.close();
                  win = null;
              }    
            };
        } catch(ex) {}
    }
    
    </script>
</head>

<body>
    <h1>Call JavaScript Function</h1>
</body>
При нажатии на кнопку в Delphi приложении в окне Firefox выполняется функция alert(). Результат представлен на картинке:
Перед перезагрузкой страницы или закрытием окна невидимое окно уничножается, его класс разрегистрируется, используемые библиотеки закрываются.
Чтобы всё это работало, требуется дать разрешения в Firefox. Для этого нужно открыть в нём окно about:config и изменить свойство signed.applets.codebase_principal_support = true. Затем открыть файл C:/Users/.../AppData/Roaming/Mozilla/Firefox/Profiles/...default/prefs.js и добавить строки
user_pref("capability.principal.codebase.p<n>.granted", "UniversalXPConnect");
user_pref("capability.principal.codebase.p<n>.id", "http://<localhost:8080>");
user_pref("capability.principal.codebase.p<n>.subjectName", "");
где <n> - порядковый номер и <localhost:8080> - адрес Вашего сервера.

вторник, 8 ноября 2011 г.

Создание PDF из карты GeoServer с русским текстом

Я установил printing-plugin в GeoServer из рекомендованной ссылки. Настроил config.yaml
fonts:
  - /usr/share/fonts/PingWin/PWTVerdeTST.ttf
  
        items:
        - !text
          text: 'Заголовок'
          font: PWTVerdeTST
          fontSize: 14
          fontEncoding: Identity-H
Всё работает, как положено. Но мне понадобилось передавать строку текста с клиента. Я заменил
text: 'Заголовок'
на
text: '${mapTitle}'
и вместо русских букв получил нечитаемые символы. При разработке приложения я использую smartgwt. Я нашёл решение этой задачи. В файле config.yaml я убрал
fontEncoding: Identity-H
В приложении я кодирую русский текст из UTF-8 в ANSI.
        // Печать текущего фрагмента карты
        MenuItem printItem = new MenuItem("Печать", skinImageDir + "print.png");
        printItem.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(MenuItemClickEvent event) {
                // Печать текущего фрагмента карты в PDF файл
                Map map = mainTabSet.getMap();
                String geoserverWMS = mainTabSet.getGeoserverWMS(); // http://localhost:8080/geoserver/wms
                String[] urlParts = geoserverWMS.split("(://|:|/)");

                UrlBuilder ub = new UrlBuilder();
                ub.setProtocol(urlParts[0]); //http
                ub.setHost(urlParts[1]); //localhost
                try {
                    ub.setPort(Integer.parseInt(urlParts[2])); //8080
                } catch (NumberFormatException e) {
                    e.printStackTrace(); // UrlBuilder.PORT_UNSPECIFIED
                }
                ub.setPath("/geoserver/pdf/print.pdf");
                java.util.Map params = new java.util.HashMap();
                params.put("units", "\"m\"");
                params.put("srs", "\"" + map.getProjection() + "\"");
                params.put("layout", "\"A4 portrait\"");
                params.put("dpi", "100");
                params.put("mapTitle", "\"Заголовок\"");
                params.put("comment", "\"Комментарий\"");
                params.put("baseURL", "\"" + geoserverWMS + "\"");
                params.put("opacity", "1");
                params.put("singleTile", "true");
                params.put("type", "\"WMS\"");
                params.put("format", "\"image/png\"");
                params.put("center", "["+map.getCenter().lon()+","+map.getCenter().lat()+"]");
                params.put("scale", Long.toString(Math.round(map.getScale())));
                params.put("rotation", "0");

                StringBuilder spec = new StringBuilder();
                spec.append("{");
                spec.append("\"units\":");       spec.append(params.get("units"));
                spec.append(",\"srs\":");        spec.append(params.get("srs"));
                spec.append(",\"layout\":");     spec.append(params.get("layout"));
                spec.append(",\"dpi\":");        spec.append(params.get("dpi"));
                spec.append(",\"mapTitle\":");   spec.append(params.get("mapTitle"));
                spec.append(",\"comment\":");    spec.append(params.get("comment"));
                spec.append(",\"layers\":");
                spec.append("[{\"baseURL\":");   spec.append(params.get("baseURL"));
                spec.append(",\"opacity\":");    spec.append(params.get("opacity"));
                spec.append(",\"singleTile\":"); spec.append(params.get("singleTile"));
                spec.append(",\"type\":");       spec.append(params.get("type"));
                spec.append(",\"layers\":");
                spec.append("[\"egko\"");
                // Получение списка включенных слоёв
                String[] s1 = mainTabSet.getArrayVisibledLayers();
                for (String s : s1)
                    if (s.equals("tp") || s.equals("rp") || s.equals("sp"))
                        spec.append(",\"rptp:main\"");
                    else {
                        spec.append(",\"rptp:");
                        spec.append(s);
                        spec.append("\"");
                    }
                spec.append("]");
                spec.append(",\"format\":");     spec.append(params.get("format"));
                spec.append(",\"styles\":");
                spec.append("[\"\"");
                for (String s : s1)
                    if (s.equals("collector_well"))
                        spec.append(",\"\"");
                    else {
                        spec.append(",\"");
                        if (s.equals("tp") || s.equals("rp") || s.equals("sp"))
                            spec.append("main_");
                        spec.append(s);
                        spec.append("\"");
                    }
                spec.append("]}]");
                spec.append(",\"pages\":");
                spec.append("[{\"center\":");   spec.append(params.get("center"));
                spec.append(",\"scale\":");     spec.append(params.get("scale"));
                spec.append(",\"rotation\":");  spec.append(params.get("rotation"));
                spec.append("}]}");
                String specVal = encodeToAnsi(spec.toString());
                ub.setParameter("spec", specVal);
                String url = URL.decode(ub.buildString());
                Window.open(url, "_top", "");
            }
        });
Здесь русский текст содержится в параметрах mapTitle и comment. Для кодирования русского текста я добавил функцию
    private native static String encodeToAnsi(String str)/*-{
        // UTF-8 to Ansi character encoding
        var hexChars = "0123456789ABCDEF";
        var trans = [];
        for (var i = 1040; i <= 1103; i++)
            trans[i] = "%" + hexChars.charAt((i >> 4) - 53) +
                    hexChars.charAt(i & 0xF); // А-Я,а-я
        trans[1025] = "%A8";      // Ё
        trans[1105] = "%B8";      // ё
        var ret = "";
        for (var i = 0; i < str.length; i++) {
            var code = str.charCodeAt(i);
            if (typeof trans[code] != 'undefined')
                ret += trans[code];
            else
                ret += str.charAt(i);
        }
        return ret;
    }-*/;
Теперь русские буквы правильно отображаются в PDF:

среда, 28 сентября 2011 г.

Исправление ошибки GeoWebCache при наличии escape последовательности в начале строки

GeoWebCache, встроенный в GeoServer, не генерировал некоторые тайлы в некоторых масштабах. При этом в geoserver.log появлялось сообщение об ошибке:
ERROR [geoserver.ows] - 
java.lang.IllegalArgumentException: 
 Zero length string passed to TextLayout constructor.
Экспериментально выяснилось, что в некоторой таблице (egko.specterr_txt) в базе данных PostGreSQL/PostGIS в поле textstring в начале строки присутствуют символы новой строки '\n'. При этом в соответствующем файле стиля specterr_txt.sld была ссылка на это поле
<Label>
  <ogc:PropertyName>textstring</ogc:PropertyName>
</Label>
После выполнения скрипта в pgAdmin III
UPDATE egko.specterr_txt SET textstring=regexp_replace(textstring, E'\n', '') 
WHERE textstring like E'\n%';
всё заработало!
Самым трудоёмким оказался поиск слоя с ошибкой в группе слоёв (Layer Groups). Мне помог Quantum GIS 1.7.0-Wroclaw, который умеет работать и с WMS, и со слоями GeoWebCache. Он сразу открывает окно с ошибкой при добавлении кривого WMS слоя. Оказалось, что из 31 слоя, присутствующего в группе, ошибка была в предпоследнем слое. Естественно, я добавлял слои последовательно, не догадался начать с конца списка.

понедельник, 11 июля 2011 г.

Получение центроида объекта из БД MS SQL Server без использования ArcSDE

Существует БД MS SQL Server с установленным ArcSDE и Spatial слоями. Требуется в веб-приложении без использования ArcSDE получить координаты точечного объекта по его идентификатору и имени таблицы.
Задача решена на C# созданием следующего метода:
    private int[] getCoordinates(SqlConnection connection, string tablename, int shape) 
    {
        int[] coord = new int[2];
        string g_table_name = "";
        SqlCommand command1 = new SqlCommand("select g_table_name from" +
            " M10_DATABASE.dbo.SDE_geometry_columns where f_table_name='" + 
            tablename + "'", connection);
        SqlDataReader reader1;
        reader1 = command1.ExecuteReader();
        if (reader1.Read())
            g_table_name = reader1["g_table_name"].ToString();
        reader1.Close();
        if (!String.IsNullOrEmpty(g_table_name))
        {
            SqlCommand command2 = new SqlCommand("select eminx, eminy, emaxx, emaxy" +
                " from M10_DATABASE.dbo." + 
                g_table_name + " where fid=" + shape, connection);
            SqlDataReader reader2 = command2.ExecuteReader();
            if (reader2.Read())
            {
                double x = ((Double)reader2["eminx"] + (Double)reader2["emaxx"]) / 2;
                double y = ((Double)reader2["eminy"] + (Double)reader2["emaxy"]) / 2;
                coord[0] = (int)Math.Round(x);
                coord[1] = (int)Math.Round(y);
            }
            reader2.Close();
        }
        return coord;
    }

понедельник, 4 июля 2011 г.

Как понизить размерность геометрического поля таблицы в PostGIS

В таблице PostGIS имеем размерность 4 (x, y, z, m). Запрос
SELECT f_geometry_column, coord_dimension, srid, type FROM geometry_columns WHERE f_table_name='main';
возвращает:
geom      4      60000      POINT
При запросе геометрических данных этой таблицы
SELECT ST_AsEWKT(geom), ST_AsText(geom), ST_NDims(geom) FROM main WHERE gid=1;
получаем:
SRID=60000;POINT(-4290.34 7248.45 0 -1.79e+308)      POINT(-4290.34 7248.45)      4
Требуется преобразовать поле geom таблицы main к размерности 2.
Для этого выполним:
ALTER TABLE main DROP CONSTRAINT enforce_dims_geom;
UPDATE geometry_columns SET coord_dimension=2 WHERE f_table_name='main';
UPDATE main SET geom=ST_GeomFromEWKT('SRID='||ST_SRID(geom)||';'||ST_AsText(geom))
        WHERE ST_NDims(geom)=4;
ALTER TABLE main ADD CONSTRAINT enforce_dims_geom CHECK (st_ndims(geom) = 2);
Всё. Задача решена. Размерность понижена с четырёх до двух.

понедельник, 27 июня 2011 г.

Добавление элементов в векторный слой с фильтрацией (gwt-openlayers)

Имеем WFS слой с некоторым атрибутом "class". Требуется показать на карте только те элементы, у которых значение class равно 1. Для этого запишем:
WFSProtocolOptions wfsProtocolOptions = new WFSProtocolOptions();
wfsProtocolOptions.setUrl("http://server:8080/geoserver/wfs");
wfsProtocolOptions.setFeatureType("main");
wfsProtocolOptions.setFeatureNameSpace("http://www.dev-city.ru/rptp");
wfsProtocolOptions.setGeometryName("geom");
wfsProtocolOptions.setVersion("1.1.0");
wfsProtocolOptions.setSrsName("EPSG:900913");
VectorOptions vectorOptions =  new VectorOptions();
vectorOptions.setProtocol(new WFSProtocol(wfsProtocolOptions));
FilterComparison filter = new FilterComparison();
filter.setType(FilterComparison.EQUAL_TO);
filter.setProperty("class");
filter.setValue(1);
vectorOptions.setStrategies(new Strategy[]{new BBoxStrategy(),
  new FilterStrategy(filter), new SaveStrategy()});
Vector layer = new Vector("слой WFS", vectorOptions);
К сожалению, в gwt-openlayers классы FilterComparison, FilterStrategy и SaveStrategy не реализованы. Но это исправимо:
package ru.rp.client.openlayers;
import org.gwtopenmaps.openlayers.client.filter.Filter;
import org.gwtopenmaps.openlayers.client.util.JSObject;

public class FilterComparison extends Filter {
    public static final short EQUAL_TO                 = 0;
    public static final short NOT_EQUAL_TO             = 1;
    public static final short LESS_THAN                = 2;
    public static final short GREATER_THAN             = 3;
    public static final short LESS_THAN_OR_EQUAL_TO    = 4;
    public static final short GREATER_THAN_OR_EQUAL_TO = 5;
    public static final short BETWEEN                  = 6;
    public static final short LIKE                     = 7;

    protected FilterComparison(JSObject filter) {
        super(filter);
    }

    public FilterComparison(){
  this(FilterComparisonImpl.create());
 }

    public void setType(short type) {
        FilterComparisonImpl.setType(this.getJSObject(), type);
    }

    public void setProperty(String property) {
        FilterComparisonImpl.setProperty(this.getJSObject(), property);
    }

    public void setValue(String value) {
        FilterComparisonImpl.setValue(this.getJSObject(), value);
    }

    public void setValue(short value) {
        FilterComparisonImpl.setValue(this.getJSObject(), value);
    }

    public void setValue(int value) {
        FilterComparisonImpl.setValue(this.getJSObject(), value);
    }

    public void setValue(float value) {
        FilterComparisonImpl.setValue(this.getJSObject(), value);
    }

}
package ru.rp.client.openlayers;

import org.gwtopenmaps.openlayers.client.util.JSObject;

public class FilterComparisonImpl {

    public native static JSObject create()/*-{
  return new $wnd.OpenLayers.Filter.Comparison();
 }-*/;

    public native static void setType(JSObject filter, short type)/*-{
        switch (type) {
            case 0: filter.type = $wnd.OpenLayers.Filter.Comparison.EQUAL_TO; break;
            case 1: filter.type = $wnd.OpenLayers.Filter.Comparison.NOT_EQUAL_TO; break;
            case 2: filter.type = $wnd.OpenLayers.Filter.Comparison.LESS_THAN; break;
            case 3: filter.type = $wnd.OpenLayers.Filter.Comparison.GREATER_THAN; break;
            case 4: filter.type = $wnd.OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO; break;
            case 5: filter.type = $wnd.OpenLayers.Filter.Comparison.GREATER_THAN_OR_EQUAL_TO; break;
            case 6: filter.type = $wnd.OpenLayers.Filter.Comparison.BETWEEN; break;
            case 7: filter.type = $wnd.OpenLayers.Filter.Comparison.LIKE; break;
        }
    }-*/;

    public native static void setProperty(JSObject filter, String property)/*-{
        filter.property = property;
    }-*/;

    public native static void setValue(JSObject filter, String value)/*-{
        filter.value = value;
    }-*/;

    public native static void setValue(JSObject filter, short value)/*-{
        filter.value = value;
    }-*/;

    public native static void setValue(JSObject filter, int value)/*-{
        filter.value = value;
    }-*/;

    public native static void setValue(JSObject filter, float value)/*-{
        filter.value = value;
    }-*/;
}
package ru.rp.client.openlayers;

import org.gwtopenmaps.openlayers.client.filter.Filter;
import org.gwtopenmaps.openlayers.client.strategy.Strategy;
import org.gwtopenmaps.openlayers.client.util.JSObject;

public class FilterStrategy extends Strategy {

    protected FilterStrategy(JSObject strategy) {
        super(strategy);
    }

    public FilterStrategy(Filter filter){
  this(FilterStrategyImpl.create(filter.getJSObject()));
    }
}

package ru.rp.client.openlayers;

import org.gwtopenmaps.openlayers.client.util.JSObject;

public class FilterStrategyImpl {

    public native static JSObject create(JSObject filter)/*-{
  return new $wnd.OpenLayers.Strategy.Filter({filter: filter});
 }-*/;

}
package ru.rp.client.openlayers;

import org.gwtopenmaps.openlayers.client.feature.VectorFeature;
import org.gwtopenmaps.openlayers.client.strategy.Strategy;
import org.gwtopenmaps.openlayers.client.util.JSObject;

public class SaveStrategy extends Strategy {

    protected SaveStrategy(JSObject strategy) {
        super(strategy);
    }

    public SaveStrategy(){
  this(SaveStrategyImpl.create());
 }

    public static void setVectorFeatureUpdateState(VectorFeature vectorFeature) {
        vectorFeature.setJSObject(SaveStrategyImpl.setVectorFeatureUpdateState(
        vectorFeature.getJSObject()));
    }

}
package ru.rp.client.openlayers;

import org.gwtopenmaps.openlayers.client.util.JSObject;

public class SaveStrategyImpl {

    public native static JSObject create()/*-{
  return new $wnd.OpenLayers.Strategy.Save();
 }-*/;

    public native static JSObject setVectorFeatureUpdateState(JSObject vector)/*-{
        if (vector.state == null)
            vector.state = $wnd.OpenLayers.State.UNKNOWN;
        vector.toState($wnd.OpenLayers.State.UPDATE);
        return vector;
    }-*/;
}
Создадим на javascript контролы добавления, редактирования и удаления точечных элементов:
        var draw = new OpenLayers.Control.DrawFeature(
            layer, OpenLayers.Handler.Point,
            {
                title: "Добавить объект",
                displayClass: "olControlDrawFeaturePoint",
                multi: false
            }
        );
        var edit = new OpenLayers.Control.ModifyFeature(layer, {
            title: "Редактировать объект",
            displayClass: "olControlModifyFeature",
            mode: OpenLayers.Control.DRAG
        });
        var del = new DeleteFeature(layer, {title: "Удалить объект"});
Эти контролы будут вставлены в панель OpenLayers.Control.Panel. Удаление и редактирование работает. Но при попытке добавления точки ничего не происходит. Если убрать new FilterStrategy(filter), то вставка будет работать.
Пришлось искать альтернативное решение, без использования FilterStrategy. Фильтрацию можно включить через стили слоя:
vectorOptions.setStyleMap(createStyleMap());
где
private StyleMap createStyleMap() {
        Style defaultStyle = new Style();
        Style selectStyle = new Style();
        Style temporaryStyle = new Style();
        // Здесь можно изменить стили
        ru.rp.client.openlayers.StyleMap styleMap =
            new ru.rp.client.openlayers.StyleMap(
                defaultStyle, selectStyle, temporaryStyle);
        styleMap.addRule(1); // Это и есть нужный фильтр
        return styleMap;
}
Для этого был создан класс:
package ru.rp.client.openlayers;

import org.gwtopenmaps.openlayers.client.Style;

public class StyleMap extends org.gwtopenmaps.openlayers.client.StyleMap {

    public StyleMap(Style defaultStyle, Style selectStyle, Style temporaryStyle) {
        super(defaultStyle, selectStyle, temporaryStyle);
    }

    public void addRule(int i) {
        StyleMapImpl.addRule(this.getJSObject(), i);
    }

}
package ru.rp.client.openlayers;

import org.gwtopenmaps.openlayers.client.util.JSObject;

public class StyleMapImpl {

    public static native void addRule(JSObject styleMap, int i)/*-{
        var rule = new $wnd.OpenLayers.Rule({
            filter: new $wnd.OpenLayers.Filter.Comparison({
                type: $wnd.OpenLayers.Filter.Comparison.EQUAL_TO,
                property: "class",
                value: i
                })
        });
        styleMap.styles["default"].addRules([rule]);
        styleMap.styles["temporary"].addRules([rule]);
        styleMap.styles["select"].addRules([rule]);
    }-*/;

}
Но это ещё не всё. При добавлении точки её атрибуты не заполнены и она не будет показана. Требуется перед добавлением точки добавить ей атрибут class с нужным значением:
layer.addVectorBeforeFeatureAddedListener(new VectorBeforeFeatureAddedListener() {
        public void onBeforeFeatureAdded(BeforeFeatureAddedEvent eventObject) {
            JSObject object = eventObject.getJSObject().getProperty("feature");
            VectorFeature vectorFeature = VectorFeature.narrowToVectorFeature(object);
            Attributes attributes = vectorFeature.getAttributes();
            if (attributes != null)
            if (attributes.getAttributeAsString("class") == null) {
                attributes.setAttribute("class", 1);
                vectorFeature.setAttributes(attributes);
            }
        }
    });
Всё! Теперь и слой фильтруется, и точки добавляются! Конечно, всё вышеизложенное можно применять сразу в openlayers (без использования gwt-openlayers).

пятница, 15 апреля 2011 г.

Запрос типа wfs:dwithin к серверу GeoServer из GWT-OpenLayers без использования gwtOpenlayersProxy

Разрабатывается веб-приложение на основе SmartGWT с поддержкой картографии. Приложение работает под Apache Tomcat. Картографические данные хранятся в PostgreSQL с использованием PostGIS. Эти данные через GeoServer в формате WMS приложение получает посредством GWT-OpenLayers.

Существенно, что GeoServer, PostgreSQL и разрабатываемое приложение располагаются на разных серверах. Потребовалось по клику на точечном объекте получить его данные из GeoServer (имя слоя и идентификатор выбранного объекта для последующего запроса к PosgreSQL).

Известно, что OpenLayers не получает ответа на запрос типа OpenLayers.loadURL, если он загружен с другого хоста. Для преодоления этого препятствия разработчиками OpenLayers было предложено использовать прокси. В GWT-OpenLayers реализован такой прокси. Это сервлет GwtOpenLayersProxyServlet, который размещён в gwt-openlayers-server-0.5.jar.

В процессе разработки выяснилось, что этот способ не работает! Помогла статья wfs:dwithin MassGIS Geospatial Web Services Wiki. В ней предложено два способа запроса к WFS GeoServer: через HTTP POST XML и через HTTP GET с параметром cql_filter.

Выполняя запрос
http://webgar:8080/geoserver/wfs?request=getfeature
&version=1.1.0&service=wfs&outputformat=json&typename=rptp:ps
&propertyName=tip,nomer_tp&cql_filter=DWITHIN(geom,POINT(15398 14449),600,meters)
я получаю нормальный ответ
{"type":"FeatureCollection","features":
[{"type":"Feature","id":"ps.9","geometry":null,"properties":
{"tip":"ПС","nomer_tp":"ПС \"Ново-Измайлово\""}}]}
При отправке того же запроса через прокси
http://localhost:8080/map_war/gwtOpenLayersProxy?targetURL=
http%3A%2F%2Fwebgar%3A8080%2Fgeoserver%2Fwfs%3Frequest%3Dgetfeature
%26version%3D1.1.0%26service%3Dwfs%26outputformat%3Djson
%26typename%3Drptp%3Aps%26propertyName%3Dtip%2Cnomer_tp
%26cql_filter%3DDWITHIN(geom%2CPOINT(15398%2014449)%2C600%2Cmeters)
возвращается ошибка
HTTP Status 500 -
type Status report
description The server encountered an internal error () that prevented it from fulfilling this request.
Apache Tomcat/7.0.10
Я попробовал убрать параметр cql_filter:
http://localhost:8080/map_war/gwtOpenLayersProxy?targetURL=
http%3A%2F%2Fwebgar%3A8080%2Fgeoserver%2Fwfs%3Frequest%3Dgetfeature%26version
%3D1.1.0%26service%3Dwfs%26outputformat%3Djson%26typename%3Drptp%3Aps
%26propertyName%3Dtip%2Cnomer_tp
В этом случае возвращается корректный ответ:
{"type":"FeatureCollection","features":
[{"type":"Feature","id":"ps.1","geometry":null,"properties":
{"tip":"ПС","nomer_tp":"53 ПС \"Герцево\""}},
{"type":"Feature","id":"ps.2","geometry":null,"properties":
{"tip":"ПС","nomer_tp":"ПС \"Матвеевская\""}},
{"type":"Feature","id":"ps.3","geometry":null,"properties":
{"tip":"ПС","nomer_tp":"ПС \"Щедрино\""}},
{"type":"Feature","id":"ps.4","geometry":null,"properties":
{"tip":"ПС","nomer_tp":"850 \"Ново-Внуково\""}},
{"type":"Feature","id":"ps.5","geometry":null,"properties":
{"tip":"ПС","nomer_tp":"ПС \"Дубнинская\""}},
{"type":"Feature","id":"ps.6","geometry":null,"properties":
{"tip":"ПС","nomer_tp":"68 ПС \"Битца\""}},
{"type":"Feature","id":"ps.7","geometry":null,"properties":
{"tip":"ПС","nomer_tp":"844 ПС \"Магистральна"}},
{"type":"Feature","id":"ps.8","geometry":null,"properties":
{"tip":"ПС","nomer_tp":"ПС \"Мневники\""}},
{"type":"Feature","id":"ps.9","geometry":null,"properties":
{"tip":"ПС","nomer_tp":"ПС \"Ново-Измайлово\""}},
{"type":"Feature","id":"ps.10","geometry":null,"properties":
{"tip":"ПС","nomer_tp":"ПС \"Горьковская\""}}]}
Выяснилось, что параметр cql_filter допустим, но при использовании DWITHIN возникает ошибка. Можно послать запрос на GeoServer утилитой curl:
curl.exe -H "Content-type: text/xml" -d @request.xml
 -X POST http://webgar:8080/geoserver/wfs > response.xml
В файле request.xml содержится тег DWithin:
<wfs:GetFeature xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/wfs
 http://webgar:8080/geoserver/schemas/wfs/1.1.0/WFS-basic.xsd"
xmlns:gml="http://www.opengis.net/gml"
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:ogc="http://www.opengis.net/ogc" service="WFS"
version="1.1.0">
  <wfs:Query typeName="rptp:ps"
  xmlns:rptp="http://rptp.dev-city.ru">
    <ogc:PropertyName>tip</ogc:PropertyName>
    <ogc:PropertyName>nomer_tp</ogc:PropertyName>
    <ogc:Filter>
      <ogc:DWithin>
        <ogc:PropertyName>geom</ogc:PropertyName>
        <gml:Point srsName="EPSG:100001"
        xmlns:gml="http://www.opengis.net/gml">
          <gml:coordinates decimal="." cs="," ts="">
          15398,14449</gml:coordinates>
        </gml:Point>
        <ogc:Distance units="meter">600</ogc:Distance>
      </ogc:DWithin>
    </ogc:Filter>
  </wfs:Query>
</wfs:GetFeature>
В результате получим response.xml:
<?xml version="1.0" encoding="utf-8"?>
<wfs:FeatureCollection numberOfFeatures="1"
timeStamp="2011-04-15T10:47:52.440+04:00"
xsi:schemaLocation="http://rptp.dev-city.ru 
http://webgar:8080/geoserver/wfs?service=WFS&amp;version=1.1.0&amp;
request=DescribeFeatureType&amp;typeName=rptp%3Aps http://www.opengis.net/wfs
 http://webgar:8080/geoserver/schemas/wfs/1.1.0/wfs.xsd"
xmlns:egko="http://egko.dev-city.ru"
xmlns:ogc="http://www.opengis.net/ogc"
xmlns:gml="http://www.opengis.net/gml"
xmlns:rptp="http://rptp.dev-city.ru"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:ows="http://www.opengis.net/ows"
xmlns:wfs="http://www.opengis.net/wfs">
  <gml:featureMembers>
    <rptp:ps gml:id="ps.9">
      <rptp:tip>ПС</rptp:tip>
      <rptp:nomer_tp>ПС "Ново-Измайлово"</rptp:nomer_tp>
    </rptp:ps>
  </gml:featureMembers>
</wfs:FeatureCollection>
Так что, поставленный эксперимент завершился удачей.
Осталось только реализовать формирование запроса и обработку ответа в нашем приложении. На стороне клиента на карте создаётся экземпляр MapClickListener и реализуется его метод onClick с параметром MapClickEvent, через который будут определяться методом getLonLat() текущие координаты x, y. Эти координаты, имя слоя и радиус поиска будут передаваться как входные параметры сервиса, который расширяет RemoteServiceServlet. Можно заготовить шаблон файла request.xml, в который будут вставляться значения этих параметров. Метод getSelectedFeatureInfo будет возвращать перечень семантических данных найденных объектов:
    @Override
    public List getSelectedFeatureInfo(double x, double y, double distance, String layer) {
        String urlStr = getServletContext().getInitParameter("geoserver-url") + "wfs";
        String requestTemplate = getServletContext().getRealPath("/") + "xml/request.xml";
        File file = new File(requestTemplate);
        List featuresInfo = null;
        try {
            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            Document doc = builder.parse(file);
            Element root = doc.getDocumentElement();
            // Установка имени слоя
            NodeList list = root.getElementsByTagName("wfs:Query");
            NamedNodeMap attr = list.item(0).getAttributes();
            Node node = attr.getNamedItem("typeName");
            node.setNodeValue(layer);
            // Установка координат
            node = root.getElementsByTagName("gml:coordinates").item(0);
            node.setTextContent(x + "," + y);
            // Установка расстояния
            node = root.getElementsByTagName("ogc:Distance").item(0);
            node.setTextContent(String.valueOf(distance));
            // Установка соединения с GeoServer
            URL url = new URL(urlStr);
            URLConnection con = url.openConnection();
            con.setDoOutput(true);
            con.setDoInput(true);
            con.setRequestProperty("Content-Type", "text/xml");
            // Формирование запроса
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            StringWriter out = new StringWriter();
            transformer.transform(new DOMSource(doc), new StreamResult(out));
            OutputStreamWriter writer = new OutputStreamWriter(con.getOutputStream());
            writer.write(out.toString());
            writer.flush();
            writer.close();
            // Получение ответа
            doc = builder.parse(con.getInputStream());
            root = doc.getDocumentElement();
            NodeList tip = root.getElementsByTagName("rptp:tip");
            NodeList nomer_tp = root.getElementsByTagName("rptp:nomer_tp");
            featuresInfo = new ArrayList(tip.getLength());
            for (int i = 0; i < tip.getLength(); i++) {
                String tipVal = tip.item(i).getTextContent();
                String nomer_tpVal = nomer_tp.item(i).getTextContent();
                FeatureInfo featureInfo = DB_RP.getFeatureInfo(tipVal, 
                    nomer_tpVal, getServletContext());
                if (featureInfo != null)
                    featuresInfo.add(i, featureInfo);
            }

        } catch (Throwable e) {
            e.printStackTrace();
        }
        return featuresInfo;
    }
В секции импорта следует добавить:
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
Всё это работает! И не потребовалось устанавливать
OpenLayers.setProxyHost("gwtOpenLayersProxy?targetURL=");

понедельник, 17 января 2011 г.

Отображение текущих координат в градусах широты и долготы в веб-приложениях ArcGIS Server

Существует некоторое веб-приложение, созданное мастером ArcGIS Server Manager. В нём используется некоторый картографический сервис с применением некоторой пользовательской проекции. Единицы измерения: метры. При перемещении указателя мыши в строке статуса браузера отображаются текущие координаты в метрах. Это выглядит так:

Пользователь выразил желание текущие координаты видеть в градусах широты и долготы. Далее по шагам описывается решение данной задачи.
1. Загрузить файл http://trac.osgeo.org/proj4js/attachment/wiki/Download/proj4js-1.0.1.zip и извлечь из него файл proj4js-combined.js, который лежит в папке lib.
2. Поместить файл proj4js-combined.js в папку c:\inetpub\wwwroot\WEBAPP\JavaScript, где WEBAPP - наше веб-приложение.
3. В файле c:\inetpub\wwwroot\WEBAPP\Default.aspx перед строкой
<script language="javascript" type="text/javascript" src="javascript/WebMapApp.js"></script>
поместить строку
<script language="javascript" type="text/javascript" src="javascript/proj4js-combined.js"></script>
4. В файле c:\inetpub\wwwroot\WEBAPP\WebMapApp.js вместо фрагмента
function MapCoordsMouseMove(sender, args) {
    var coords = args.coordinate;
    var coordsSeparator = (webMapAppDecimalDelimiter == ",") ? "  " : ", ";
    var roundFactor = Math.pow(10, arcgisWebApp.CoordsDecimals);
    var xstring = (Math.round(coords.get_x() * roundFactor) / roundFactor).toString();
    var ystring = (Math.round(coords.get_y() * roundFactor) / roundFactor).toString();
    if (webMapAppDecimalDelimiter == ",") {
        xstring = xstring.replace(/\./g, webMapAppDecimalDelimiter);
        ystring = ystring.replace(/\./g, webMapAppDecimalDelimiter);
    }
    window.status = xstring + coordsSeparator + ystring;
}

вставить фрагмент
Proj4js.defs["EPSG:100001"] = "+proj=tmerc +lat_0=55.667 +lon_0=37.5 +k=1 +x_0=0 +y_0=0 +ellps=krass +units=m +no_defs";
var src = new Proj4js.Proj("EPSG:100001");
var dst = new Proj4js.Proj("WGS84");

function MapCoordsMouseMove(sender, args) {
    var coords = args.coordinate;
    var coordsSeparator = (webMapAppDecimalDelimiter == ",") ? "  " : ", ";
    var roundFactor = Math.pow(10, arcgisWebApp.CoordsDecimals);
    var p = new Proj4js.Point(coords.get_x(), coords.get_y());
    Proj4js.transform(src, dst, p);
    var xstring = (Math.round(p.x * roundFactor) / roundFactor).toString();
    var ystring = (Math.round(p.y * roundFactor) / roundFactor).toString();
    if (webMapAppDecimalDelimiter == ",") {
        xstring = xstring.replace(/\./g, webMapAppDecimalDelimiter);
        ystring = ystring.replace(/\./g, webMapAppDecimalDelimiter);
    }
    window.status = xstring + coordsSeparator + ystring;
}
Всё! Результат получен. В строке статуса можно увидеть координаты в градусах широты и долготы: